Socket
Socket
Sign inDemoInstall

@mtproto/core

Package Overview
Dependencies
11
Maintainers
1
Versions
43
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.0.7 to 0.0.8

src/__tests__/index.html

18

package.json
{
"name": "@mtproto/core",
"version": "0.0.7",
"description": "Telegram API for browser",
"version": "0.0.8",
"description": "Telegram API (MTProto) client library for browser",
"keywords": [

@@ -9,5 +9,11 @@ "telegram",

"mtproto",
"mtproto2",
"tdlib",
"browser",
"api"
"client",
"library",
"lib",
"api",
"messenger",
"telegram-web",
"telegram-online"
],

@@ -32,2 +38,3 @@ "homepage": "https://github.com/alik0211/mtproto-core#readme",

"axios": "0.19.0",
"big-integer": "1.6.48",
"events": "3.1.0",

@@ -41,6 +48,7 @@ "leemon": "6.2.0",

"dotenv-webpack": "1.7.0",
"html-webpack-plugin": "3.2.0",
"webpack": "4.41.2",
"webpack-cli": "3.3.10",
"webpack-dev-server": "3.9.0"
"webpack-dev-server": "3.10.3"
}
}

@@ -5,5 +5,7 @@ # @mtproto/core

> Library for working with the Telegram API in pure JavaScript.
> Telegram API (MTProto) client library for browser
* **Relevant.** 108 layer in the API scheme
* **Easy.** Cryptography is hidden. Just make requests to the API
* **Events.** Subscribe to updates via the EventEmitter API
* **Reliable.** Supports all data centers. Automatically handles `MIGRATE` errors

@@ -14,35 +14,89 @@ const MTProto = require('../main');

const phoneNumber = '+9996620000';
// Ali: +9996611111 -> @test9996611111
// Pavel: +9996622222 -> @test9996622222
// Ivan: +9996627777 -> @test9996627777
const phoneNumber = '+9996621111';
mtproto.api
.call('users.getFullUser', {
id: {
_: 'inputUserSelf',
},
})
.then(result => {
console.log(`users.getFullUser[result]:`, result);
})
.catch(() => {
mtproto.api
.call('auth.sendCode', {
phone_number: phoneNumber,
settings: {
_: 'codeSettings',
},
})
.then(result => {
return mtproto.api.call('auth.signIn', {
phone_code: '22222',
phone_number: phoneNumber,
phone_code_hash: result.phone_code_hash,
});
})
.then(result => {
console.log('auth.signIn[result]:', result);
});
const formCode = document.getElementById('form-code');
const formPhone = document.getElementById('form-phone');
const getFullUser = document.getElementById('getFullUser');
const formPassword = document.getElementById('form-password');
const servers = document.querySelectorAll('.page__servers .page__server');
let phone = null;
let phoneCodeHash = null;
let code = null;
let password = null;
formPhone.addEventListener('submit', event => {
event.preventDefault();
phone = formPhone.elements.phone.value;
console.log(`phone:`, phone);
mtproto.api
.call('auth.sendCode', {
phone_number: phone,
settings: {
_: 'codeSettings',
},
})
.then(result => {
phoneCodeHash = result.phone_code_hash;
console.log(`phoneCodeHash:`, phoneCodeHash);
});
});
formCode.addEventListener('submit', event => {
event.preventDefault();
code = formCode.elements.code.value;
mtproto.api
.call('auth.signIn', {
phone_code: code,
phone_number: phone,
phone_code_hash: phoneCodeHash,
})
.then(result => {
console.log(`auth.signIn[result]:`, result);
})
.catch(error => {
if (error.error_message === 'SESSION_PASSWORD_NEEDED') {
console.log(`Need password!`);
}
});
});
formPassword.addEventListener('submit', event => {
event.preventDefault();
password = formPassword.elements.password.value;
// console.log(`password:`, password);
mtproto.api.checkPassword(password).then(result => {
console.log(`auth.checkPassword[result]:`, result);
});
});
mtproto.api.on('updateShort', message => {
console.log(`updateShort[message]:`, message);
servers.forEach(button => {
button.addEventListener('click', () => {
const { id } = button.dataset;
console.log(`id:`, id);
mtproto.api.setDc(id);
});
});
getFullUser.addEventListener('click', () => {
mtproto.api
.call('users.getFullUser', {
id: {
_: 'inputUserSelf',
},
})
.then(result => {
console.log(`users.getFullUser[result]:`, result);
});
});

@@ -1,3 +0,1231 @@

const API = require('./api');
const bigInt = require('big-integer');
const debounce = require('lodash.debounce');
const EventEmitter = require('events');
const http = require('./transport');
const { SecureRandom } = require('./vendors/jsbn');
const { TLSerialization, TLDeserialization } = require('./tl');
const {
getSRPParams,
arrayBufferToBase64,
bigStringInt,
bytesToHex,
bytesFromHex,
bytesCmp,
bytesXor,
bytesToArrayBuffer,
convertToArrayBuffer,
convertToUint8Array,
bytesFromArrayBuffer,
bufferConcat,
longToBytes,
longFromInts,
sha1BytesSync,
sha256HashSync,
rsaEncrypt,
aesEncryptSync,
aesDecryptSync,
nextRandomInt,
pqPrimeFactorization,
bytesModPow,
getNonce,
getAesKeyIv,
tsNow,
} = require('./utils');
const RsaKeysManager = require('./utils/rsa');
const secureRandom = new SecureRandom();
function Deferred() {
this.resolve = null;
this.reject = null;
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
Object.freeze(this);
}
class API extends EventEmitter {
constructor({ api_id, api_hash, test, https }) {
super();
this.api_id = api_id;
this.api_hash = api_hash;
this.test = test;
this.https = https;
this.localTime = tsNow();
this.lastMessageId = [0, 0];
this.timeOffset = 0;
this._seqNo = 0;
this.sessionId = null;
this.prevSessionId = null;
this.longPollRunning = false;
// TODO: Use Map()
this.sentMessages = {};
this.pendingAcks = [];
this.sendAcks = debounce(() => {
if (!this.pendingAcks.length) {
return;
}
// console.log(`JSON.stringify(pendingAcks):`, JSON.stringify(pendingAcks));
var waitSerializer = new TLSerialization({ mtproto: true });
waitSerializer.storeMethod('http_wait', {
max_delay: 500,
wait_after: 150,
max_wait: 1000,
});
const waitMessage = {
msg_id: this.generateMessageId(),
seq_no: this.generateSeqNo(),
body: waitSerializer.getBytes(),
};
const serializer = new TLSerialization({ mtproto: true });
serializer.storeObject(
{ _: 'msgs_ack', msg_ids: this.pendingAcks },
'Object'
);
const message = {
msg_id: this.generateMessageId(),
seq_no: this.generateSeqNo(true),
body: serializer.getBytes(),
};
this.pendingAcks = [];
this.sendEncryptedRequest([waitMessage, message]);
}, 500);
this.updateSession();
this.setDc();
}
init() {
const serverSalt = this.getServerSalt();
const authKey = this.getAuthKey();
if (serverSalt && authKey) {
this.setServerSalt(serverSalt);
this.setAuthKey(authKey);
this.runLongPoll();
return Promise.resolve();
}
const nonce = getNonce();
const request = new TLSerialization({ mtproto: true });
request.storeMethod('req_pq_multi', { nonce });
return this.sendPlainRequest(request.getBuffer()).then(deserializer => {
const responsePQ = deserializer.fetchObject('ResPQ');
console.log('2. response', responsePQ);
if (responsePQ._ != 'resPQ') {
throw new Error('[MT] resPQ response invalid: ' + responsePQ._);
}
if (!bytesCmp(nonce, responsePQ.nonce)) {
throw new Error('[MT] resPQ nonce mismatch');
}
const serverNonce = responsePQ.server_nonce;
const pq = responsePQ.pq;
const publicKey = RsaKeysManager.select(
responsePQ.server_public_key_fingerprints
);
// console.log(
// 'Got ResPQ',
// bytesToHex(responsePQ.server_nonce),
// bytesToHex(responsePQ.pq),
// responsePQ.server_public_key_fingerprints
// );
if (!publicKey) {
throw new Error('[MT] No public key found');
}
const [p, q] = pqPrimeFactorization(pq);
const newNonce = new Array(32);
secureRandom.nextBytes(newNonce);
const data = new TLSerialization({ mtproto: true });
data.storeObject(
{
_: 'p_q_inner_data',
pq: pq,
p: p,
q: q,
nonce: nonce,
server_nonce: serverNonce,
new_nonce: newNonce,
},
'P_Q_inner_data',
'DECRYPTED_DATA'
);
const dataWithHash = sha1BytesSync(data.getBuffer()).concat(
data.getBytes()
);
const request = new TLSerialization({ mtproto: true });
request.storeMethod('req_DH_params', {
nonce: nonce,
server_nonce: serverNonce,
p: p,
q: q,
public_key_fingerprint: publicKey.fingerprint,
encrypted_data: rsaEncrypt(publicKey, dataWithHash),
});
return this.sendPlainRequest(request.getBuffer()).then(deserializer => {
const responseDH = deserializer.fetchObject(
'Server_DH_Params',
'RESPONSE'
);
console.log('3. responseDH', responseDH);
if (
responseDH._ != 'server_DH_params_fail' &&
responseDH._ != 'server_DH_params_ok'
) {
throw new Error(
'[MT] Server_DH_Params response invalid: ' + responseDH._
);
}
if (!bytesCmp(nonce, responseDH.nonce)) {
throw new Error('[MT] Server_DH_Params nonce mismatch');
}
if (!bytesCmp(serverNonce, responseDH.server_nonce)) {
throw new Error('[MT] Server_DH_Params server_nonce mismatch');
}
if (responseDH._ == 'server_DH_params_fail') {
var newNonceHash = sha1BytesSync(newNonce).slice(-16);
if (!bytesCmp(newNonceHash, responseDH.new_nonce_hash)) {
throw new Error(
'[MT] server_DH_params_fail new_nonce_hash mismatch'
);
}
throw new Error('[MT] server_DH_params_fail');
}
this.localTime = tsNow();
const tmpAesKey = sha1BytesSync(newNonce.concat(serverNonce)).concat(
sha1BytesSync(serverNonce.concat(newNonce)).slice(0, 12)
);
const tmpAesIv = sha1BytesSync(serverNonce.concat(newNonce))
.slice(12)
.concat(
sha1BytesSync([].concat(newNonce, newNonce)),
newNonce.slice(0, 4)
);
const answerWithHash = aesDecryptSync(
responseDH.encrypted_answer,
tmpAesKey,
tmpAesIv
);
var hash = answerWithHash.slice(0, 20);
var answerWithPadding = answerWithHash.slice(20);
var buffer = bytesToArrayBuffer(answerWithPadding);
var deserializer = new TLDeserialization(buffer, { mtproto: true });
const responseDHInner = deserializer.fetchObject(
'Server_DH_inner_data'
);
console.log('4. responseDHInner', responseDHInner);
if (responseDHInner._ != 'server_DH_inner_data') {
throw new Error(
'[MT] server_DH_inner_data response invalid: ' + constructor
);
}
if (!bytesCmp(nonce, responseDHInner.nonce)) {
throw new Error('[MT] server_DH_inner_data nonce mismatch');
}
if (!bytesCmp(serverNonce, responseDHInner.server_nonce)) {
throw new Error('[MT] server_DH_inner_data serverNonce mismatch');
}
console.log('5. Done decrypting answer');
const g = responseDHInner.g;
const dhPrime = responseDHInner.dh_prime;
const gA = responseDHInner.g_a;
const retry = 0;
console.log('6. verifyDhParams start');
this.verifyDhParams(g, dhPrime, gA);
console.log('7. verifyDhParams finish');
var offset = deserializer.getOffset();
if (
!bytesCmp(hash, sha1BytesSync(answerWithPadding.slice(0, offset)))
) {
throw new Error('[MT] server_DH_inner_data SHA1-hash mismatch');
}
this.applyServerTime(responseDHInner.server_time);
return this.sendSetClientDhParams({
nonce,
serverNonce,
newNonce,
tmpAesKey,
tmpAesIv,
g,
dhPrime,
gA,
retry,
}).then(() => {
this.runLongPoll();
return Promise.resolve();
});
});
});
}
verifyDhParams(g, dhPrime, gA) {
console.log('Verifying DH params');
const dhPrimeHex = bytesToHex(dhPrime);
if (
g != 3 ||
dhPrimeHex !==
'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b'
) {
// The verified value is from https://core.telegram.org/mtproto/security_guidelines
throw new Error('[MT] DH params are not verified: unknown dhPrime');
}
console.log('dhPrime cmp OK');
const gABigInt = bigInt(bytesToHex(gA), 16);
const dhPrimeBigInt = bigInt(dhPrimeHex, 16);
if (gABigInt.compareTo(bigInt.one) <= 0) {
throw new Error('[MT] DH params are not verified: gA <= 1');
}
if (gABigInt.compareTo(dhPrimeBigInt.subtract(bigInt.one)) >= 0) {
throw new Error('[MT] DH params are not verified: gA >= dhPrime - 1');
}
console.log('1 < gA < dhPrime-1 OK');
const twoPow = bigInt(2).pow(2048 - 64);
if (gABigInt.compareTo(twoPow) < 0) {
throw new Error('[MT] DH params are not verified: gA < 2^{2048-64}');
}
if (gABigInt.compareTo(dhPrimeBigInt.subtract(twoPow)) >= 0) {
throw new Error(
'[MT] DH params are not verified: gA > dhPrime - 2^{2048-64}'
);
}
console.log('2^{2048-64} < gA < dhPrime-2^{2048-64} OK');
return true;
}
sendSetClientDhParams(auth) {
var gBytes = bytesFromHex(auth.g.toString(16));
auth.b = new Array(256);
secureRandom.nextBytes(auth.b);
const gB = bytesModPow(gBytes, auth.b, auth.dhPrime);
var data = new TLSerialization({ mtproto: true });
data.storeObject(
{
_: 'client_DH_inner_data',
nonce: auth.nonce,
server_nonce: auth.serverNonce,
retry_id: [0, auth.retry++],
g_b: gB,
},
'Client_DH_Inner_Data'
);
var dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes());
var encryptedData = aesEncryptSync(
dataWithHash,
auth.tmpAesKey,
auth.tmpAesIv
);
var request = new TLSerialization({ mtproto: true });
request.storeMethod('set_client_DH_params', {
nonce: auth.nonce,
server_nonce: auth.serverNonce,
encrypted_data: encryptedData,
});
console.log('Send set_client_DH_params');
return this.sendPlainRequest(request.getBuffer()).then(deserializer => {
var response = deserializer.fetchObject('Set_client_DH_params_answer');
if (
response._ != 'dh_gen_ok' &&
response._ != 'dh_gen_retry' &&
response._ != 'dh_gen_fail'
) {
throw new Error(
'[MT] Set_client_DH_params_answer response invalid: ' + response._
);
}
if (!bytesCmp(auth.nonce, response.nonce)) {
throw new Error('[MT] Set_client_DH_params_answer nonce mismatch');
}
if (!bytesCmp(auth.serverNonce, response.server_nonce)) {
throw new Error(
'[MT] Set_client_DH_params_answer server_nonce mismatch'
);
}
const authKey = bytesModPow(auth.gA, auth.b, auth.dhPrime);
const authKeyHash = sha1BytesSync(authKey);
const authKeyAux = authKeyHash.slice(0, 8);
const authKeyId = authKeyHash.slice(-8);
console.log('Got Set_client_DH_params_answer', response._, response);
switch (response._) {
case 'dh_gen_ok':
var newNonceHash1 = sha1BytesSync(
auth.newNonce.concat([1], authKeyAux)
).slice(-16);
if (!bytesCmp(newNonceHash1, response.new_nonce_hash1)) {
throw new Error(
'[MT] Set_client_DH_params_answer new_nonce_hash1 mismatch'
);
}
var serverSalt = bytesXor(
auth.newNonce.slice(0, 8),
auth.serverNonce.slice(0, 8)
);
console.log('Auth successfull!', authKeyId, authKey, serverSalt);
auth.authKeyId = authKeyId;
this.setAuthKey(authKey);
this.setServerSalt(serverSalt);
return auth;
case 'dh_gen_retry':
var newNonceHash2 = sha1BytesSync(
auth.newNonce.concat([2], authKeyAux)
).slice(-16);
if (!bytesCmp(newNonceHash2, response.new_nonce_hash2)) {
throw new Error(
'[MT] Set_client_DH_params_answer new_nonce_hash2 mismatch'
);
}
return this.sendSetClientDhParams(auth);
case 'dh_gen_fail':
var newNonceHash3 = sha1BytesSync(
auth.newNonce.concat([3], authKeyAux)
).slice(-16);
if (!bytesCmp(newNonceHash3, response.new_nonce_hash3)) {
throw new Error(
'[MT] Set_client_DH_params_answer new_nonce_hash3 mismatch'
);
}
throw new Error('[MT] Set_client_DH_params_answer fail');
}
});
}
sendEncryptedRequest(messages) {
// console.log(`sendEncryptedRequest[messages]:`, messages);
let resultMessage = messages;
if (Array.isArray(messages)) {
const messagesByteLen = messages.reduce(
(acc, message) =>
acc + (message.body.byteLength || message.body.length) + 32,
0
);
//create container;
var container = new TLSerialization({
mtproto: true,
startMaxLength: messagesByteLen + 64,
});
container.storeInt(0x73f1f8dc, 'CONTAINER[id]');
container.storeInt(messages.length, 'CONTAINER[count]');
var onloads = [];
var innerMessages = [];
for (var i = 0; i < messages.length; i++) {
container.storeLong(messages[i].msg_id, 'CONTAINER[' + i + '][msg_id]');
innerMessages.push(messages[i].msg_id);
/* sentMessages[messages[i].msg_id] = messages[i];
* sentMessages[messages[i].msg_id].inContainer = true; */
container.storeInt(messages[i].seq_no, 'CONTAINER[' + i + '][seq_no]');
container.storeInt(
messages[i].body.length,
'CONTAINER[' + i + '][bytes]'
);
container.storeRawBytes(messages[i].body, 'CONTAINER[' + i + '][body]');
/* if (messages[i].noResponse) {
* //noResponseMsgs.push(messages[i].msg_id);
* } */
}
const containerSentMessage = {
msg_id: this.generateMessageId(),
seq_no: this.generateSeqNo(true),
container: true,
inner: innerMessages,
};
resultMessage = {
...{ body: container.getBytes(true) },
...containerSentMessage,
};
this.sentMessages[resultMessage.msg_id] = containerSentMessage;
}
return this._sendEncryptedRequest(resultMessage).then(responsePackage => {
// console.log(`responsePackage:`, responsePackage);
const { response, messageId } = responsePackage;
this.processMessage(response, messageId);
this.sendAcks();
return responsePackage;
});
}
_sendEncryptedRequest(message) {
const authKey = this.getAuthKey();
const authKeyUint8 = convertToUint8Array(authKey);
const authKeyId = sha1BytesSync(authKey).slice(-8);
const serverSalt = this.getServerSalt();
var data = new TLSerialization({
startMaxLength: message.body.length + 2048,
});
message.deferred = message.deferred || new Deferred();
this.sentMessages[message.msg_id] = message;
data.storeIntBytes(serverSalt, 64, 'salt');
data.storeIntBytes(this.sessionId, 64, 'session_id');
data.storeLong(message.msg_id, 'message_id');
data.storeInt(message.seq_no, 'seq_no');
data.storeInt(message.body.length, 'message_data_length');
data.storeRawBytes(message.body, 'message_data');
var dataBuffer = data.getBuffer();
var paddingLength = 16 - (data.offset % 16) + 16 * (1 + nextRandomInt(5));
var padding = new Array(paddingLength);
secureRandom.nextBytes(padding);
var dataWithPadding = bufferConcat(dataBuffer, padding);
const encryptedResult = this.getEncryptedMessage(
dataWithPadding,
authKeyUint8
);
//console.log('encryptedResult.msgKey', encryptedResult.msgKey, dHexDump(encryptedResult.msgKey));
var request = new TLSerialization({
startMaxLength: encryptedResult.bytes.byteLength + 256,
});
request.storeIntBytes(authKeyId, 64, 'auth_key_id');
request.storeIntBytes(encryptedResult.msgKey, 128, 'msg_key');
request.storeRawBytes(encryptedResult.bytes, 'encrypted_data');
var requestData = request.getArray();
return http
.post(this.url, requestData, {
responseType: 'arraybuffer',
transformRequest: null,
})
.then(result => {
if (!result.data || !result.data.byteLength) {
throw new Error('No data');
}
const self = this;
const responseBuffer = result.data;
var responseDeserializer = new TLDeserialization(responseBuffer);
const serverAuthKeyId = responseDeserializer.fetchIntBytes(
64,
false,
'auth_key_id'
);
if (!bytesCmp(serverAuthKeyId, authKeyId)) {
throw new Error(
'[MT] Invalid server auth_key_id: ' + bytesToHex(serverAuthKeyId)
);
}
var msgKey = responseDeserializer.fetchIntBytes(128, true, 'msg_key');
var encryptedData = responseDeserializer.fetchRawBytes(
responseBuffer.byteLength - responseDeserializer.getOffset(),
true,
'encrypted_data'
);
const dataWithPadding = this.getDecryptedMessage(
authKeyUint8,
msgKey,
encryptedData
);
const calcMsgKey = this.getMsgKey(authKeyUint8, dataWithPadding, false);
//console.log(msgKey, calcMsgKey, dHexDump(msgKey), dHexDump(calcMsgKey));
if (!bytesCmp(msgKey, calcMsgKey)) {
console.warn(
'[MT] msg_keys',
msgKey,
bytesFromArrayBuffer(calcMsgKey)
);
throw new Error('[MT] server msgKey mismatch');
}
var dataDeserializer = new TLDeserialization(dataWithPadding, {
mtproto: true,
});
var salt = dataDeserializer.fetchIntBytes(64, false, 'salt');
var serverSessionId = dataDeserializer.fetchIntBytes(
64,
false,
'session_id'
);
var messageId = dataDeserializer.fetchLong('message_id');
if (
!bytesCmp(serverSessionId, this.sessionId) &&
(!this.prevSessionId || !bytesCmp(this.sessionId, this.prevSessionId))
) {
console.warn(
'Sessions',
serverSessionId,
this.sessionId,
this.prevSessionId
);
throw new Error(
'[MT] Invalid server session_id: ' + bytesToHex(serverSessionId)
);
}
var seqNo = dataDeserializer.fetchInt('seq_no');
var totalLength = dataWithPadding.byteLength;
var messageBodyLength = dataDeserializer.fetchInt(
'message_data[length]'
);
if (
messageBodyLength % 4 ||
messageBodyLength > totalLength - dataDeserializer.getOffset()
) {
throw new Error('[MT] Invalid body length: ' + messageBodyLength);
}
var messageBody = dataDeserializer.fetchRawBytes(
messageBodyLength,
true,
'message_data'
);
var paddingLength = totalLength - dataDeserializer.getOffset();
if (paddingLength < 12 || paddingLength > 1024) {
throw new Error('[MT] Invalid padding length: ' + paddingLength);
}
var buffer = bytesToArrayBuffer(messageBody);
var deserializerOptions = {
mtproto: true,
override: {
mt_message: function(result, field) {
result.msg_id = this.fetchLong(field + '[msg_id]');
result.seqno = this.fetchInt(field + '[seqno]');
result.bytes = this.fetchInt(field + '[bytes]');
var offset = this.getOffset();
try {
result.body = this.fetchObject('Object', field + '[body]');
} catch (e) {
console.error('parse error', e.message, e.stack);
result.body = { _: 'parse_error', error: e };
}
if (this.offset != offset + result.bytes) {
this.offset = offset + result.bytes;
}
},
mt_rpc_result: function(result, field) {
result.req_msg_id = this.fetchLong(field + '[req_msg_id]');
var sentMessage = self.sentMessages[result.req_msg_id];
var type = (sentMessage && sentMessage.resultType) || 'Object';
if (result.req_msg_id && !sentMessage) {
return;
}
result.result = this.fetchObject(type, field + '[result]');
},
},
};
var finalDeserializer = new TLDeserialization(
buffer,
deserializerOptions
);
var response = finalDeserializer.fetchObject('', 'INPUT');
return {
response,
messageId,
sessionId: this.sessionId,
seqNo,
messageDeferred: message.deferred.promise,
};
});
}
sendPlainRequest(requestBuffer) {
const requestLength = requestBuffer.byteLength;
const requestArray = new Int32Array(requestBuffer);
const header = new TLSerialization();
header.storeLongP(0, 0, 'auth_key_id');
header.storeLong(this.generateMessageId(), 'msg_id');
header.storeInt(requestLength, 'request_length');
const headerBuffer = header.getBuffer();
const headerArray = new Int32Array(headerBuffer);
const headerLength = headerBuffer.byteLength;
const resultBuffer = new ArrayBuffer(headerLength + requestLength);
const resultArray = new Int32Array(resultBuffer);
resultArray.set(headerArray);
resultArray.set(requestArray, headerArray.length);
const requestData = resultArray;
return http
.post(this.url, requestData, {
responseType: 'arraybuffer',
transformRequest: null,
})
.then(function(result) {
if (!result.data || !result.data.byteLength) {
throw new Error('no data');
}
var deserializer = new TLDeserialization(result.data, {
mtproto: true,
});
var auth_key_id = deserializer.fetchLong('auth_key_id');
var msg_id = deserializer.fetchLong('msg_id');
var msg_len = deserializer.fetchInt('msg_len');
return deserializer;
});
}
getEncryptedMessage(dataWithPadding, authKeyUint8) {
const msgKey = this.getMsgKey(authKeyUint8, dataWithPadding, true);
const keyIv = getAesKeyIv(authKeyUint8, msgKey, true);
// console.log('after msg key iv')
//convertToArrayBuffer(aesEncryptSync(dataWithPadding, msgKey, keyIv)) ?
const encryptedBytes = convertToArrayBuffer(
aesEncryptSync(dataWithPadding, keyIv[0], keyIv[1])
);
return {
bytes: encryptedBytes,
msgKey: msgKey,
};
}
getDecryptedMessage(authKeyUint8, msgKey, encryptedData) {
const keyIv = getAesKeyIv(authKeyUint8, msgKey, false);
return convertToArrayBuffer(
aesDecryptSync(encryptedData, keyIv[0], keyIv[1])
);
}
processMessage(message, messageId) {
// console.log('processMessage', message, messageId);
let sentMessage;
switch (message._) {
case 'msg_container':
var len = message.messages.length;
for (var i = 0; i < len; i++) {
this.processMessage(message.messages[i], message.messages[i].msg_id);
}
break;
case 'bad_server_salt':
console.log('Bad server salt');
sentMessage = this.sentMessages[message.bad_msg_id];
if (!sentMessage || sentMessage.seq_no != message.bad_msg_seqno) {
console.log(message.bad_msg_id, message.bad_msg_seqno);
throw new Error('[MT] Bad server salt for invalid message');
}
this.setServerSalt(longToBytes(message.new_server_salt));
this.sendEncryptedRequest(this.sentMessages[message.bad_msg_id]);
this.ackMessage(messageId);
break;
case 'bad_msg_notification':
console.log('Bad msg notification', message);
sentMessage = this.sentMessages[message.bad_msg_id];
if (!sentMessage || sentMessage.seq_no != message.bad_msg_seqno) {
console.log(message.bad_msg_id, message.bad_msg_seqno);
throw new Error('[MT] Bad msg notification for invalid message');
}
if (message.error_code == 16 || message.error_code == 17) {
if (
this.applyServerTime(
bigStringInt(messageId)
.shiftRight(32)
.toString(10)
)
) {
console.log('Update session');
this.updateSession();
}
this.sendEncryptedRequest(this.sentMessages[message.bad_msg_id]);
this.ackMessage(messageId);
}
break;
case 'message':
this.ackMessage(messageId);
this.processMessage(message.body, message.msg_id);
break;
case 'new_session_created':
this.ackMessage(messageId);
this.processMessageAck(message.first_msg_id);
this.setServerSalt(longToBytes(message.server_salt));
break;
case 'msgs_ack':
for (var i = 0; i < message.msg_ids.length; i++) {
this.processMessageAck(message.msg_ids[i]);
}
break;
case 'msg_detailed_info':
//console.log('msg_detailed_info', message);
break;
/* if (!this.sentMessages[message.msg_id]) {
* this.ackMessage(message.answer_msg_id)
* break
* } */
case 'msg_new_detailed_info':
//console.log('msg_detailed_info', message);
break;
/* if (this.pendingAcks.indexOf(message.answer_msg_id)) {
* break
* }
* this.reqResendMessage(message.answer_msg_id)
* break */
case 'msgs_state_info':
this.ackMessage(message.answer_msg_id);
console.log('msgs_state_info', message);
/* if (this.lastResendReq && this.lastResendReq.req_msg_id == message.req_msg_id && this.pendingResends.length) {
* var i, badMsgId, pos
* for (i = 0; i < this.lastResendReq.resend_msg_ids.length; i++) {
* badMsgId = this.lastResendReq.resend_msg_ids[i]
* pos = this.pendingResends.indexOf(badMsgId)
* if (pos != -1) {
* this.pendingResends.splice(pos, 1)
* }
* }
* } */
break;
case 'rpc_result':
const sentMessageId = message.req_msg_id;
this.ackMessage(messageId);
this.processMessageAck(sentMessageId);
if (this.sentMessages[sentMessageId]) {
const { deferred } = this.sentMessages[sentMessageId];
deferred.resolve(message);
delete this.sentMessages[sentMessageId];
}
break;
default:
console.log('default', message);
this.ackMessage(messageId);
this.emit(message._, message);
// console.log('processMessage', message, messageId);
break;
}
}
ackMessage(messageId) {
// console.log('ackMessage[messageId]:', messageId);
this.pendingAcks.push(messageId);
}
processMessageAck(messageId) {
const sentMessage = this.sentMessages[messageId];
if (sentMessage && !sentMessage.acked) {
delete sentMessage.body;
sentMessage.acked = true;
return true;
}
return false;
}
applyServerTime(serverTime) {
const newTimeOffset = serverTime - Math.floor(this.localTime / 1000);
const changed = Math.abs(this.timeOffset - newTimeOffset) > 10;
this.lastMessageId = [0, 0];
this.timeOffset = newTimeOffset;
console.log(
'Apply server time',
serverTime,
this.localTime,
newTimeOffset,
changed
);
return changed;
}
updateSession() {
this.prevSessionId = this.sessionId;
this.sessionId = new Array(8);
secureRandom.nextBytes(this.sessionId);
this._seqNo = 0;
}
getMsgKey(authKeyUint8, dataWithPadding, isOut) {
var authKey = authKeyUint8;
var x = isOut ? 0 : 8;
var msgKeyLargePlain = bufferConcat(
authKey.subarray(88 + x, 88 + x + 32),
dataWithPadding
);
const msgKeyLarge = sha256HashSync(msgKeyLargePlain);
return new Uint8Array(msgKeyLarge).subarray(8, 24);
}
generateSeqNo(notContentRelated) {
var seqNo = this._seqNo * 2;
if (!notContentRelated) {
seqNo += 1;
this._seqNo += 1;
}
return seqNo;
}
generateMessageId() {
const timeTicks = tsNow();
const timeSec = Math.floor(timeTicks / 1000) + this.timeOffset;
const timeMSec = timeTicks % 1000;
const random = nextRandomInt(0xffff);
const { lastMessageId } = this;
let messageId = [timeSec, (timeMSec << 21) | (random << 3) | 4];
if (
lastMessageId[0] > messageId[0] ||
(lastMessageId[0] == messageId[0] && lastMessageId[1] >= messageId[1])
) {
messageId = [lastMessageId[0], lastMessageId[1] + 4];
}
this.lastMessageId = messageId;
return longFromInts(messageId[0], messageId[1]);
}
getServerSalt() {
const key = `dc${this.dcId}ServerSalt`;
const fromThis = this[key];
if (fromThis) {
return fromThis;
}
const fromStorage = localStorage.getItem(key);
if (fromStorage) {
return JSON.parse(fromStorage);
}
return null;
}
setServerSalt(serverSalt) {
const key = `dc${this.dcId}ServerSalt`;
this[key] = serverSalt;
localStorage.setItem(key, JSON.stringify(serverSalt));
}
getAuthKey() {
const key = `dc${this.dcId}AuthKey`;
const fromThis = this[key];
if (fromThis) {
return fromThis;
}
const fromStorage = localStorage.getItem(key);
if (fromStorage) {
return JSON.parse(fromStorage);
}
return null;
}
setAuthKey(authKey) {
const key = `dc${this.dcId}AuthKey`;
this[key] = authKey;
localStorage.setItem(key, JSON.stringify(authKey));
}
setDc(dcId) {
const fromStorage = localStorage.getItem('dcId', dcId);
this.dcId = dcId || fromStorage || 2;
localStorage.setItem('dcId', this.dcId);
const subdomainsMap = {
1: 'pluto',
2: 'venus',
3: 'aurora',
4: 'vesta',
5: 'flora',
};
const ipMap = this.test
? {
1: '149.154.175.10',
2: '149.154.167.40',
3: '149.154.175.117',
}
: {
1: '149.154.175.50',
2: '149.154.167.51',
3: '149.154.175.100',
4: '149.154.167.91',
5: '149.154.171.5',
};
const urlPath = this.test ? '/apiw_test1' : '/apiw1';
if (this.https) {
this.url = `https://${
subdomainsMap[this.dcId]
}.web.telegram.org${urlPath}`;
} else {
this.url = `http://${ipMap[this.dcId]}${urlPath}`;
}
}
runLongPoll() {
if (this.longPollRunning) {
return;
}
this.longPollRunning = true;
const longPollInner = () => {
const serializer = new TLSerialization({ mtproto: true });
serializer.storeMethod('http_wait', {
max_delay: 500,
wait_after: 150,
max_wait: 15000,
});
const message = {
msg_id: this.generateMessageId(),
seq_no: this.generateSeqNo(),
body: serializer.getBytes(),
};
this.sendEncryptedRequest(message).finally(longPollInner);
};
longPollInner();
}
getApiCallMessage(method, params = {}) {
const serializer = new TLSerialization();
serializer.storeInt(0xda9b0d0d, 'invokeWithLayer');
serializer.storeInt(108, 'layer');
serializer.storeInt(0x785188b8, 'initConnection');
serializer.storeInt(0, 'flags'); // because the proxy is not set
serializer.storeInt(this.api_id, 'api_id');
serializer.storeString(
navigator.userAgent || 'Unknown UserAgent',
'device_model'
);
serializer.storeString(
navigator.platform || 'Unknown Platform',
'system_version'
);
serializer.storeString('1.0.0', 'app_version');
serializer.storeString(navigator.language || 'en', 'system_lang_code');
serializer.storeString('', 'lang_pack');
serializer.storeString(navigator.language || 'en', 'lang_code');
serializer.storeMethod(method, {
api_hash: this.api_hash,
api_id: this.api_id,
...params,
});
let toAck = []; //msgs_ack
const message = {
msg_id: this.generateMessageId(),
seq_no: this.generateSeqNo(),
body: serializer.getBytes(true),
isAPI: true,
method,
};
const messageByteLength =
(message.body.byteLength || message.body.length) + 32;
return message;
}
innerCall(method, params) {
return this.init().then(() => {
const message = this.getApiCallMessage(method, params);
this.sendAcks();
return new Promise((resolve, reject) => {
this.sendEncryptedRequest(message)
.then(response => {
const { messageDeferred } = response;
messageDeferred.then(message => {
if (message.result._ === 'rpc_error') {
reject(message.result);
} else {
resolve(message.result);
}
});
})
.catch(reject);
});
});
}
call(method, params) {
return this.innerCall(method, params).catch(error => {
console.log(`error:`, error);
const { error_message } = error;
if (error_message.includes('_MIGRATE_')) {
const [_type, dcId] = error_message.split('_MIGRATE_');
this.setDc(dcId);
return this.call(method, params);
}
throw error;
});
}
checkPassword(password) {
return this.call('account.getPassword').then(async result => {
const { srp_id, current_algo, secure_random, srp_B } = result;
const { salt1, salt2, g, p } = current_algo;
const { A, M1 } = await getSRPParams({
g,
p,
salt1,
salt2,
gB: srp_B,
password,
});
return this.call('auth.checkPassword', {
password: {
_: 'inputCheckPasswordSRP',
srp_id,
A,
M1,
},
});
});
}
getFileInBase64({ location, offset = 0, limit = 1024 * 1024 }) {
return this.call('upload.getFile', {
flags: 0,
offset,
limit,
location,
}).then(response => {
return arrayBufferToBase64(response.bytes);
});
}
}
class MTProto {

@@ -4,0 +1232,0 @@ constructor({ api_id, api_hash, test = false, https = false }) {

const { Zlib } = require('zlibjs/bin/gunzip.min.js');
const Rusha = require('rusha');
const bigInt = require('big-integer');
const {

@@ -23,2 +24,152 @@ powMod,

function bigIntToBytes(bigInt, len) {
return hexToBytes(bigInt.toString(16), len);
}
function hexToBytes(str, len) {
if (!len) {
len = Math.ceil(str.length / 2);
}
while (str.length < len * 2) {
str = '0' + str;
}
const buf = new Uint8Array(len);
for (let i = 0; i < len; i++) {
buf[i] = parseInt(str.slice(i * 2, i * 2 + 2), 16);
}
return buf;
}
function bytesToBigInt(bytes) {
const digits = new Array(bytes.byteLength);
for (let i = 0; i < bytes.byteLength; i++) {
digits[i] =
bytes[i] < 16 ? '0' + bytes[i].toString(16) : bytes[i].toString(16);
}
return bigInt(digits.join(''), 16);
}
function randomBytes(len) {
const bytes = new Uint8Array(len);
crypto.getRandomValues(bytes);
return bytes;
}
function xorBytes(bytes1, bytes2) {
let bytes = new Uint8Array(bytes1.byteLength);
for (let i = 0; i < bytes1.byteLength; i++) {
bytes[i] = bytes1[i] ^ bytes2[i];
}
return bytes;
}
function concatBytes(...arrays) {
let totalLength = 0;
for (let bytes of arrays) {
if (typeof bytes === 'number') {
// padding
totalLength = Math.ceil(totalLength / bytes) * bytes;
} else {
totalLength += bytes.byteLength;
}
}
let merged = new Uint8Array(totalLength);
let offset = 0;
for (let bytes of arrays) {
if (typeof bytes === 'number') {
merged.set(randomBytes(totalLength - offset), offset);
} else {
merged.set(
bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes,
offset
);
offset += bytes.byteLength;
}
}
return merged;
}
async function SHA256(data) {
return new Uint8Array(await crypto.subtle.digest('SHA-256', data));
}
async function PBKDF2(hash, password, salt, iterations) {
return new Uint8Array(
await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
hash,
salt,
iterations,
},
await crypto.subtle.importKey(
'raw',
password,
{ name: 'PBKDF2' },
false,
['deriveBits']
),
512
)
);
}
async function getSRPParams({ g, p, salt1, salt2, gB, password }) {
const H = SHA256;
const SH = (data, salt) => {
return SHA256(concatBytes(salt, data, salt));
};
const PH1 = async (password, salt1, salt2) => {
return await SH(await SH(password, salt1), salt2);
};
const PH2 = async (password, salt1, salt2) => {
return await SH(
await PBKDF2('SHA-512', await PH1(password, salt1, salt2), salt1, 100000),
salt2
);
};
const encoder = new TextEncoder();
const gBigInt = bigInt(g);
const gBytes = bigIntToBytes(gBigInt, 256);
const pBigInt = bytesToBigInt(p);
const aBigInt = bytesToBigInt(randomBytes(256));
const gABigInt = gBigInt.modPow(aBigInt, pBigInt);
const gABytes = bigIntToBytes(gABigInt);
const gBBytes = bytesToBigInt(gB);
const [k, u, x] = await Promise.all([
H(concatBytes(p, gBytes)),
H(concatBytes(gABytes, gB)),
PH2(encoder.encode(password), salt1, salt2),
]);
const kBigInt = bytesToBigInt(k);
const uBigInt = bytesToBigInt(u);
const xBigInt = bytesToBigInt(x);
const vBigInt = gBigInt.modPow(xBigInt, pBigInt);
const kVBigInt = kBigInt.multiply(vBigInt).mod(pBigInt);
let tBigInt = gBBytes.subtract(kVBigInt).mod(pBigInt);
if (tBigInt.isNegative()) {
tBigInt = tBigInt.add(pBigInt);
}
const sABigInt = tBigInt.modPow(
aBigInt.add(uBigInt.multiply(xBigInt)),
pBigInt
);
const sABytes = bigIntToBytes(sABigInt);
const kA = await H(sABytes);
const M1 = await H(
concatBytes(
xorBytes(await H(p), await H(gBytes)),
await H(salt1),
await H(salt2),
gABytes,
gB,
kA
)
);
return { A: gABytes, M1 };
}
function bigint(num) {

@@ -380,5 +531,5 @@ return new BigInteger(num.toString(16), 16);

function sha256HashSync(bytes) {
// console.log(dT(), 'SHA-2 hash start', bytes.byteLength || bytes.length)
// console.log(dT(), 'SHA-256 hash start', bytes.byteLength || bytes.length)
var hashWords = CryptoJS.SHA256(bytesToWords(bytes));
// console.log(dT(), 'SHA-2 hash finish')
// console.log(dT(), 'SHA-256 hash finish')

@@ -788,2 +939,11 @@ var hashBytes = bytesFromWords(hashWords);

module.exports = {
bigIntToBytes,
hexToBytes,
bytesToBigInt,
randomBytes,
xorBytes,
concatBytes,
SHA256,
PBKDF2,
getSRPParams,
bigint,

@@ -790,0 +950,0 @@ bigStringInt,

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc