@mtproto/core
Advanced tools
Comparing version 0.0.8 to 1.0.0
{ | ||
"name": "@mtproto/core", | ||
"version": "0.0.8", | ||
"version": "1.0.0", | ||
"description": "Telegram API (MTProto) client library for browser", | ||
@@ -32,7 +32,9 @@ "keywords": [ | ||
"start": "webpack-dev-server --color", | ||
"build": "webpack --mode production" | ||
"build": "webpack --mode production", | ||
"watch": "karma start --browsers Chrome", | ||
"test": "karma start --browsers ChromeCI --single-run" | ||
}, | ||
"license": "GPL-3.0", | ||
"dependencies": { | ||
"axios": "0.19.0", | ||
"aes-js": "3.1.2", | ||
"big-integer": "1.6.48", | ||
@@ -48,2 +50,8 @@ "events": "3.1.0", | ||
"html-webpack-plugin": "3.2.0", | ||
"jasmine-core": "3.5.0", | ||
"karma": "4.4.1", | ||
"karma-chrome-launcher": "3.1.0", | ||
"karma-jasmine": "3.1.1", | ||
"karma-mocha-reporter": "2.2.5", | ||
"karma-webpack": "4.0.2", | ||
"webpack": "4.41.2", | ||
@@ -50,0 +58,0 @@ "webpack-cli": "3.3.10", |
# @mtproto/core | ||
[![NPM](https://img.shields.io/npm/v/@mtproto/core.svg?style=flat-square)](https://www.npmjs.com/package/@mtproto/core) | ||
[![Travis](https://img.shields.io/travis/com/alik0211/mtproto-core/master.svg?style=flat-square)](https://travis-ci.com/alik0211/mtproto-core) | ||
> Telegram API (MTProto) client library for browser | ||
* **Relevant.** 108 layer in the API scheme | ||
* **Actual.** 108 layer in the API scheme | ||
* **Fast.** Uses WebSocket to work with the network | ||
* **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 | ||
* **2FA.** Use the library's built-in function to calculate 2FA parameters | ||
## Install | ||
``` | ||
yarn add @mtproto/core -E | ||
``` | ||
## Quick start | ||
You need **api_id** and **api_hash**. If you do not have them yet, then get them according to the official instructions: [creating your Telegram application](https://core.telegram.org/api/obtaining_api_id). | ||
```js | ||
const MTProto = require('@mtproto/core'); | ||
// 1. Create an instance | ||
const mtproto = new MTProto({ | ||
api_id: YOU_API_ID, | ||
api_hash: YOU_API_HASH, | ||
// Use test server | ||
test: true, | ||
}); | ||
// 2. Log in using phone number | ||
// https://core.telegram.org/api/auth#test-phone-numbers | ||
const phone = '+9996621111'; | ||
const code = '22222'; | ||
mtproto | ||
.call('auth.sendCode', { | ||
phone_number: phone, | ||
settings: { | ||
_: 'codeSettings', | ||
}, | ||
}) | ||
.then(result => { | ||
mtproto | ||
.call('auth.signIn', { | ||
phone_code: code, | ||
phone_number: phone, | ||
phone_code_hash: result.phone_code_hash, | ||
}) | ||
.then(result => { | ||
console.log(`auth.signIn[result]:`, result); | ||
}) | ||
.catch(error => { | ||
if (error.error_message === 'SESSION_PASSWORD_NEEDED') { | ||
// Need use 2FA | ||
} | ||
}); | ||
}); | ||
``` | ||
## 2FA (Two-factor authentication) | ||
```js | ||
const { getSRPParams } = require('@mtproto/core/utils'); | ||
const password = 'YOU_PASSWORD'; | ||
mtproto | ||
.call('account.getPassword') | ||
.then(async result => { | ||
const { srp_id, current_algo, srp_B } = result; | ||
const { salt1, salt2, g, p } = current_algo; | ||
const { A, M1 } = await getSRPParams({ | ||
g, | ||
p, | ||
salt1, | ||
salt2, | ||
gB: srp_B, | ||
password, | ||
}); | ||
return mtproto.call('auth.checkPassword', { | ||
password: { | ||
_: 'inputCheckPasswordSRP', | ||
srp_id, | ||
A, | ||
M1, | ||
}, | ||
}); | ||
}) | ||
.then(result => { | ||
console.log(`auth.checkPassword[result]:`, result); | ||
}); | ||
``` |
1538
src/main.js
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 TLSerializer = require('./tl/serializer'); | ||
const TLDeserializer = require('./tl/deserializer'); | ||
const { | ||
getSRPParams, | ||
arrayBufferToBase64, | ||
bigStringInt, | ||
bytesToHex, | ||
bytesFromHex, | ||
bytesCmp, | ||
bytesXor, | ||
getRandomBytes, | ||
concatBytes, | ||
bytesToBigInt, | ||
bigIntToBytes, | ||
bytesToArrayBuffer, | ||
convertToArrayBuffer, | ||
convertToUint8Array, | ||
bytesFromArrayBuffer, | ||
bufferConcat, | ||
longToBytes, | ||
longFromInts, | ||
sha1BytesSync, | ||
sha256HashSync, | ||
rsaEncrypt, | ||
aesEncryptSync, | ||
aesDecryptSync, | ||
nextRandomInt, | ||
getRandomInt, | ||
pqPrimeFactorization, | ||
bytesModPow, | ||
getNonce, | ||
getAesKeyIv, | ||
tsNow, | ||
convertToByteArray, | ||
xorBytes, | ||
gzipUncompress, | ||
} = require('./utils'); | ||
const RsaKeysManager = require('./utils/rsa'); | ||
const { AES, SHA1, SHA256 } = require('./utils/crypto'); | ||
const { getRsaKeyByFingerprints } = require('./utils/rsa'); | ||
const secureRandom = new SecureRandom(); | ||
const defaultDC = 2; | ||
function Deferred() { | ||
this.resolve = null; | ||
this.reject = null; | ||
this.promise = new Promise((resolve, reject) => { | ||
this.resolve = resolve; | ||
this.reject = reject; | ||
}); | ||
Object.freeze(this); | ||
} | ||
class Storage { | ||
constructor(prefix) { | ||
this._prefix = prefix; | ||
} | ||
class API extends EventEmitter { | ||
constructor({ api_id, api_hash, test, https }) { | ||
super(); | ||
setPrefix(prefix) { | ||
this._prefix = prefix; | ||
} | ||
this.api_id = api_id; | ||
this.api_hash = api_hash; | ||
this.test = test; | ||
this.https = https; | ||
// Set with prefix | ||
pSet(name, value) { | ||
const key = `${this._prefix}${name}`; | ||
this[key] = value; | ||
this.localTime = tsNow(); | ||
this.lastMessageId = [0, 0]; | ||
this.timeOffset = 0; | ||
this._seqNo = 0; | ||
this.sessionId = null; | ||
this.prevSessionId = null; | ||
this.longPollRunning = false; | ||
localStorage[key] = JSON.stringify(value); | ||
} | ||
// TODO: Use Map() | ||
this.sentMessages = {}; | ||
this.pendingAcks = []; | ||
pGetBytes(name) { | ||
return new Uint8Array(this.pGet(name)); | ||
} | ||
this.sendAcks = debounce(() => { | ||
if (!this.pendingAcks.length) { | ||
return; | ||
} | ||
// Get with prefix | ||
pGet(name) { | ||
const key = `${this._prefix}${name}`; | ||
// console.log(`JSON.stringify(pendingAcks):`, JSON.stringify(pendingAcks)); | ||
if (key in this) { | ||
return this[key]; | ||
} | ||
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(), | ||
}; | ||
if (key in localStorage) { | ||
this[key] = JSON.parse(localStorage[key]); | ||
const serializer = new TLSerialization({ mtproto: true }); | ||
serializer.storeObject( | ||
{ _: 'msgs_ack', msg_ids: this.pendingAcks }, | ||
'Object' | ||
); | ||
return this[key]; | ||
} | ||
const message = { | ||
msg_id: this.generateMessageId(), | ||
seq_no: this.generateSeqNo(true), | ||
body: serializer.getBytes(), | ||
}; | ||
return null; | ||
} | ||
this.pendingAcks = []; | ||
set(key, value) { | ||
this[key] = value; | ||
this.sendEncryptedRequest([waitMessage, message]); | ||
}, 500); | ||
this.updateSession(); | ||
this.setDc(); | ||
localStorage[key] = JSON.stringify(value); | ||
} | ||
init() { | ||
const serverSalt = this.getServerSalt(); | ||
const authKey = this.getAuthKey(); | ||
get(key) { | ||
if (key in this) { | ||
return this[key]; | ||
} | ||
if (serverSalt && authKey) { | ||
this.setServerSalt(serverSalt); | ||
this.setAuthKey(authKey); | ||
if (key in localStorage) { | ||
this[key] = JSON.parse(localStorage[key]); | ||
this.runLongPoll(); | ||
return Promise.resolve(); | ||
return this[key]; | ||
} | ||
const nonce = getNonce(); | ||
const request = new TLSerialization({ mtproto: true }); | ||
request.storeMethod('req_pq_multi', { nonce }); | ||
return null; | ||
} | ||
} | ||
return this.sendPlainRequest(request.getBuffer()).then(deserializer => { | ||
const responsePQ = deserializer.fetchObject('ResPQ'); | ||
console.log('2. response', responsePQ); | ||
class MTProto { | ||
constructor({ api_id, api_hash, test = false }) { | ||
this.api_id = api_id; | ||
this.api_hash = api_hash; | ||
this.test = test; | ||
if (responsePQ._ != 'resPQ') { | ||
throw new Error('[MT] resPQ response invalid: ' + responsePQ._); | ||
} | ||
this.messagesWaitResponse = new Map(); | ||
this.messagesWaitAuth = []; | ||
this.pendingAcks = []; | ||
if (!bytesCmp(nonce, responsePQ.nonce)) { | ||
throw new Error('[MT] resPQ nonce mismatch'); | ||
} | ||
this.storage = new Storage(); | ||
this.storage.set('timeOffset', 0); | ||
const serverNonce = responsePQ.server_nonce; | ||
const pq = responsePQ.pq; | ||
const publicKey = RsaKeysManager.select( | ||
responsePQ.server_public_key_fingerprints | ||
); | ||
this.updateSession(); | ||
this.setDc(); | ||
// console.log( | ||
// 'Got ResPQ', | ||
// bytesToHex(responsePQ.server_nonce), | ||
// bytesToHex(responsePQ.pq), | ||
// responsePQ.server_public_key_fingerprints | ||
// ); | ||
this.handleWSError = this.handleWSError.bind(this); | ||
this.handleWSOpen = this.handleWSOpen.bind(this); | ||
this.handleWSClose = this.handleWSClose.bind(this); | ||
this.handleWSMessage = this.handleWSMessage.bind(this); | ||
if (!publicKey) { | ||
throw new Error('[MT] No public key found'); | ||
} | ||
this.connect(); | ||
} | ||
async handleWSError(event) {} | ||
async handleWSOpen(event) { | ||
const initialMessage = await this.generateObfuscationKeys(); | ||
this.socket.send(initialMessage); | ||
const [p, q] = pqPrimeFactorization(pq); | ||
const authKey = this.storage.pGet('authKey'); | ||
const serverSalt = this.storage.pGet('serverSalt'); | ||
const newNonce = new Array(32); | ||
secureRandom.nextBytes(newNonce); | ||
if (authKey && serverSalt) { | ||
this.handleMessage = this.handleEncryptedMessage; | ||
} else { | ||
this.nonce = getRandomBytes(16); | ||
this.handleMessage = this.handlePQResponse; | ||
this.sendPlainMessage('req_pq_multi', { nonce: this.nonce }); | ||
} | ||
} | ||
async handleWSClose(event) { | ||
this.recconect(); | ||
} | ||
async handleWSMessage(event) { | ||
const fileReader = new FileReader(); | ||
fileReader.onload = async event => { | ||
const obfuscatedBytes = new Uint8Array(event.target.result); | ||
const buffer = await this.deobfuscate(obfuscatedBytes); | ||
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' | ||
); | ||
this.handleMessage(buffer); | ||
}; | ||
fileReader.readAsArrayBuffer(event.data); | ||
} | ||
const dataWithHash = sha1BytesSync(data.getBuffer()).concat( | ||
data.getBytes() | ||
); | ||
async handlePQResponse(buffer) { | ||
const deserializer = new TLDeserializer(buffer, { mtproto: true }); | ||
const auth_key_id = deserializer.long('auth_key_id'); | ||
const msg_id = deserializer.long('msg_id'); | ||
const msg_len = deserializer.int('msg_len'); | ||
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), | ||
}); | ||
const responsePQ = deserializer.predicate('ResPQ'); | ||
const { | ||
pq, | ||
nonce, | ||
server_nonce, | ||
server_public_key_fingerprints, | ||
} = responsePQ; | ||
return this.sendPlainRequest(request.getBuffer()).then(deserializer => { | ||
const responseDH = deserializer.fetchObject( | ||
'Server_DH_Params', | ||
'RESPONSE' | ||
); | ||
console.log('3. responseDH', responseDH); | ||
const publicKey = await getRsaKeyByFingerprints( | ||
server_public_key_fingerprints | ||
); | ||
if ( | ||
responseDH._ != 'server_DH_params_fail' && | ||
responseDH._ != 'server_DH_params_ok' | ||
) { | ||
throw new Error( | ||
'[MT] Server_DH_Params response invalid: ' + responseDH._ | ||
); | ||
} | ||
const [p, q] = pqPrimeFactorization(pq); | ||
if (!bytesCmp(nonce, responseDH.nonce)) { | ||
throw new Error('[MT] Server_DH_Params nonce mismatch'); | ||
} | ||
this.newNonce = getRandomBytes(32); | ||
this.serverNonce = server_nonce; | ||
if (!bytesCmp(serverNonce, responseDH.server_nonce)) { | ||
throw new Error('[MT] Server_DH_Params server_nonce mismatch'); | ||
} | ||
const serializer = new TLSerializer({ mtproto: true }); | ||
serializer.predicate( | ||
{ | ||
_: 'p_q_inner_data', | ||
pq: pq, | ||
p: p, | ||
q: q, | ||
nonce: this.nonce, | ||
server_nonce: this.serverNonce, | ||
new_nonce: this.newNonce, | ||
}, | ||
'P_Q_inner_data' | ||
); | ||
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'); | ||
} | ||
const data = serializer.getBytes(); | ||
const dataHash = await SHA1(data); | ||
this.localTime = tsNow(); | ||
const innerData = getRandomBytes(255); | ||
innerData.set(dataHash); | ||
innerData.set(data, dataHash.length); | ||
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 encryptedData = rsaEncrypt(publicKey, innerData); | ||
const answerWithHash = aesDecryptSync( | ||
responseDH.encrypted_answer, | ||
tmpAesKey, | ||
tmpAesIv | ||
); | ||
this.sendPlainMessage('req_DH_params', { | ||
nonce: this.nonce, | ||
server_nonce: this.serverNonce, | ||
p: p, | ||
q: q, | ||
public_key_fingerprint: publicKey.fingerprint, | ||
encrypted_data: encryptedData, | ||
}); | ||
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(); | ||
}); | ||
}); | ||
}); | ||
this.handleMessage = this.handleDHParams; | ||
} | ||
verifyDhParams(g, dhPrime, gA) { | ||
console.log('Verifying DH params'); | ||
const dhPrimeHex = bytesToHex(dhPrime); | ||
async handleDHParams(buffer) { | ||
const deserializer = new TLDeserializer(buffer, { mtproto: true }); | ||
const auth_key_id = deserializer.long('auth_key_id'); | ||
const msg_id = deserializer.long('msg_id'); | ||
const msg_len = deserializer.int('msg_len'); | ||
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'); | ||
} | ||
const serverDH = deserializer.predicate('Server_DH_Params'); | ||
console.log('dhPrime cmp OK'); | ||
this.tmpAesKey = concatBytes( | ||
await SHA1(concatBytes(this.newNonce, this.serverNonce)), | ||
(await SHA1(concatBytes(this.serverNonce, this.newNonce))).slice(0, 12) | ||
); | ||
this.tmpAesIV = concatBytes( | ||
(await SHA1(concatBytes(this.serverNonce, this.newNonce))).slice(12, 20), | ||
await SHA1(concatBytes(this.newNonce, this.newNonce)), | ||
this.newNonce.slice(0, 4) | ||
); | ||
const gABigInt = bigInt(bytesToHex(gA), 16); | ||
const dhPrimeBigInt = bigInt(dhPrimeHex, 16); | ||
const decryptedData = new AES.IGE(this.tmpAesKey, this.tmpAesIV).decrypt( | ||
serverDH.encrypted_answer | ||
); | ||
const innerDataHash = decryptedData.slice(0, 20); | ||
const innerDeserializer = new TLDeserializer( | ||
decryptedData.slice(20).buffer, | ||
{ | ||
mtproto: true, | ||
isPlain: true, | ||
} | ||
); | ||
if (gABigInt.compareTo(bigInt.one) <= 0) { | ||
throw new Error('[MT] DH params are not verified: gA <= 1'); | ||
} | ||
const serverDHInnerData = innerDeserializer.predicate( | ||
'Server_DH_inner_data' | ||
); | ||
if (gABigInt.compareTo(dhPrimeBigInt.subtract(bigInt.one)) >= 0) { | ||
throw new Error('[MT] DH params are not verified: gA >= dhPrime - 1'); | ||
} | ||
this.storage.set( | ||
'timeOffset', | ||
Math.floor(Date.now() / 1000) - serverDHInnerData.server_time | ||
); | ||
console.log('1 < gA < dhPrime-1 OK'); | ||
this.dhPrime = bytesToBigInt(serverDHInnerData.dh_prime); | ||
this.g = bigInt(serverDHInnerData.g); | ||
this.gA = bytesToBigInt(serverDHInnerData.g_a); | ||
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; | ||
this.generateDH(); | ||
} | ||
sendSetClientDhParams(auth) { | ||
var gBytes = bytesFromHex(auth.g.toString(16)); | ||
async generateDH(retryId = 0) { | ||
const b = bytesToBigInt(getRandomBytes(256)); | ||
const authKey = convertToByteArray( | ||
bigIntToBytes(this.gA.modPow(b, this.dhPrime)) | ||
); | ||
const serverSalt = convertToByteArray( | ||
xorBytes(this.newNonce.slice(0, 8), this.serverNonce.slice(0, 8)) | ||
); | ||
auth.b = new Array(256); | ||
secureRandom.nextBytes(auth.b); | ||
this.storage.pSet('authKey', authKey); | ||
this.storage.pSet('serverSalt', serverSalt); | ||
const gB = bytesModPow(gBytes, auth.b, auth.dhPrime); | ||
var data = new TLSerialization({ mtproto: true }); | ||
data.storeObject( | ||
const innerSerializer = new TLSerializer({ mtproto: true }); | ||
innerSerializer.predicate( | ||
{ | ||
_: 'client_DH_inner_data', | ||
nonce: auth.nonce, | ||
server_nonce: auth.serverNonce, | ||
retry_id: [0, auth.retry++], | ||
g_b: gB, | ||
nonce: this.nonce, | ||
server_nonce: this.serverNonce, | ||
retry_id: retryId, | ||
g_b: bigIntToBytes(this.g.modPow(b, this.dhPrime)), | ||
}, | ||
'Client_DH_Inner_Data' | ||
); | ||
const innerData = innerSerializer.getBytes(); | ||
var dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes()); | ||
var encryptedData = aesEncryptSync( | ||
dataWithHash, | ||
auth.tmpAesKey, | ||
auth.tmpAesIv | ||
const encryptedData = new AES.IGE(this.tmpAesKey, this.tmpAesIV).encrypt( | ||
concatBytes(await SHA1(innerData), innerData, 16) | ||
); | ||
var request = new TLSerialization({ mtproto: true }); | ||
request.storeMethod('set_client_DH_params', { | ||
nonce: auth.nonce, | ||
server_nonce: auth.serverNonce, | ||
this.sendPlainMessage('set_client_DH_params', { | ||
nonce: this.nonce, | ||
server_nonce: this.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'); | ||
} | ||
}); | ||
this.handleMessage = this.handleDHAnswer; | ||
} | ||
sendEncryptedRequest(messages) { | ||
// console.log(`sendEncryptedRequest[messages]:`, messages); | ||
async handleDHAnswer(buffer) { | ||
const deserializer = new TLDeserializer(buffer, { mtproto: true }); | ||
const auth_key_id = deserializer.long('auth_key_id'); | ||
const msg_id = deserializer.long('msg_id'); | ||
const msg_len = deserializer.int('msg_len'); | ||
let resultMessage = messages; | ||
const serverDHAnswer = deserializer.predicate( | ||
'Set_client_DH_params_answer' | ||
); | ||
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; | ||
if (serverDHAnswer._ === 'dh_gen_ok') { | ||
this.handleMessage = this.handleEncryptedMessage; | ||
this.handleAuth(); | ||
} else { | ||
console.error(`Invalid Set_client_DH_params_answer:`, serverDHAnswer); | ||
} | ||
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, | ||
async handleAuth() { | ||
this.messagesWaitAuth.forEach(message => { | ||
const { method, params, resolve, reject } = message; | ||
this.call(method, params) | ||
.then(resolve) | ||
.catch(reject); | ||
}); | ||
message.deferred = message.deferred || new Deferred(); | ||
this.sentMessages[message.msg_id] = message; | ||
this.messagesWaitAuth = []; | ||
} | ||
data.storeIntBytes(serverSalt, 64, 'salt'); | ||
data.storeIntBytes(this.sessionId, 64, 'session_id'); | ||
async handleEncryptedMessage(buffer) { | ||
const authKey = this.storage.pGetBytes('authKey'); | ||
data.storeLong(message.msg_id, 'message_id'); | ||
data.storeInt(message.seq_no, 'seq_no'); | ||
const deserializer = new TLDeserializer(buffer); | ||
const authKeyId = deserializer.long(); | ||
const messageKey = deserializer.int128(); | ||
data.storeInt(message.body.length, 'message_data_length'); | ||
data.storeRawBytes(message.body, 'message_data'); | ||
const encryptedData = deserializer.byteView.slice(deserializer.offset); | ||
var dataBuffer = data.getBuffer(); | ||
const plaintextData = ( | ||
await this.getAESInstance(authKey, messageKey, true) | ||
).decrypt(encryptedData); | ||
var paddingLength = 16 - (data.offset % 16) + 16 * (1 + nextRandomInt(5)); | ||
var padding = new Array(paddingLength); | ||
secureRandom.nextBytes(padding); | ||
const plainDeserializer = new TLDeserializer(plaintextData.buffer, { | ||
isPlain: true, | ||
}); | ||
var dataWithPadding = bufferConcat(dataBuffer, padding); | ||
const salt = plainDeserializer.long(); | ||
const sessionId = plainDeserializer.long(); | ||
const messageId = plainDeserializer.long(); | ||
const seqNo = plainDeserializer.uint32(); | ||
const length = plainDeserializer.uint32(); | ||
const encryptedResult = this.getEncryptedMessage( | ||
dataWithPadding, | ||
authKeyUint8 | ||
); | ||
const result = plainDeserializer.predicate(); | ||
//console.log('encryptedResult.msgKey', encryptedResult.msgKey, dHexDump(encryptedResult.msgKey)); | ||
this.handleDecryptedMessage(result, { messageId, seqNo }); | ||
} | ||
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'); | ||
async handleDecryptedMessage(message, params = {}) { | ||
// console.group(`handleDecryptedMessage ${message._}`); | ||
// console.log(`message:`, message); | ||
// console.log(`params:`, params); | ||
// console.groupEnd(`handleDecryptedMessage ${message._}`); | ||
var requestData = request.getArray(); | ||
const { messageId } = params; | ||
let waitMessage = null; | ||
return http | ||
.post(this.url, requestData, { | ||
responseType: 'arraybuffer', | ||
transformRequest: null, | ||
}) | ||
.then(result => { | ||
if (!result.data || !result.data.byteLength) { | ||
throw new Error('No data'); | ||
} | ||
switch (message._) { | ||
case 'msg_container': | ||
message.messages.forEach(message => { | ||
this.handleDecryptedMessage(message.body, { | ||
messageId: message.msg_id, | ||
}); | ||
}); | ||
return; | ||
const self = this; | ||
case 'bad_server_salt': | ||
waitMessage = this.messagesWaitResponse.get(message.bad_msg_id); | ||
const responseBuffer = result.data; | ||
var responseDeserializer = new TLDeserialization(responseBuffer); | ||
const serverAuthKeyId = responseDeserializer.fetchIntBytes( | ||
64, | ||
false, | ||
'auth_key_id' | ||
); | ||
if (!bytesCmp(serverAuthKeyId, authKeyId)) { | ||
if (!waitMessage) { | ||
throw new Error( | ||
'[MT] Invalid server auth_key_id: ' + bytesToHex(serverAuthKeyId) | ||
`bad_server_salt. Not found message width id ${message.bad_msg_id}` | ||
); | ||
} | ||
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); | ||
this.storage.pSet('serverSalt', longToBytes(message.new_server_salt)); | ||
this.call(waitMessage.method, waitMessage.params) | ||
.then(waitMessage.resolve) | ||
.catch(waitMessage.reject); | ||
this.messagesWaitResponse.delete(message.bad_msg_id); | ||
this.ackMessage(messageId); | ||
return; | ||
//console.log(msgKey, calcMsgKey, dHexDump(msgKey), dHexDump(calcMsgKey)); | ||
case 'bad_msg_notification': | ||
return; | ||
if (!bytesCmp(msgKey, calcMsgKey)) { | ||
console.warn( | ||
'[MT] msg_keys', | ||
msgKey, | ||
bytesFromArrayBuffer(calcMsgKey) | ||
); | ||
throw new Error('[MT] server msgKey mismatch'); | ||
} | ||
case 'new_session_created': | ||
this.ackMessage(messageId); | ||
var dataDeserializer = new TLDeserialization(dataWithPadding, { | ||
mtproto: true, | ||
}); | ||
// this.messagesWaitResponse.delete(message.first_msg_id); | ||
this.storage.pSet('serverSalt', longToBytes(message.server_salt)); | ||
var salt = dataDeserializer.fetchIntBytes(64, false, 'salt'); | ||
var serverSessionId = dataDeserializer.fetchIntBytes( | ||
64, | ||
false, | ||
'session_id' | ||
); | ||
var messageId = dataDeserializer.fetchLong('message_id'); | ||
return; | ||
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) | ||
); | ||
} | ||
case 'msgs_ack': | ||
// console.log(`msgs_ack:`, message.msg_ids); | ||
// console.log(`this.messagesWaitResponse:`, this.messagesWaitResponse); | ||
// message.msg_ids.forEach(msgId => { | ||
// this.pendingAcks.forEach((pendingAckMsgId, index) => { | ||
// if (msgId === pendingAckMsgId) { | ||
// this.pendingAcks.splice(index, 1); | ||
// } | ||
// }); | ||
// }); | ||
return; | ||
var seqNo = dataDeserializer.fetchInt('seq_no'); | ||
case 'rpc_result': | ||
this.ackMessage(messageId); | ||
var totalLength = dataWithPadding.byteLength; | ||
if (message.result._ === 'gzip_packed') { | ||
const uncompressed = bytesToArrayBuffer( | ||
gzipUncompress(message.result.packed_data) | ||
); | ||
var messageBodyLength = dataDeserializer.fetchInt( | ||
'message_data[length]' | ||
); | ||
const deserializer = new TLDeserializer(uncompressed, { | ||
isPlain: true, | ||
}); | ||
if ( | ||
messageBodyLength % 4 || | ||
messageBodyLength > totalLength - dataDeserializer.getOffset() | ||
) { | ||
throw new Error('[MT] Invalid body length: ' + messageBodyLength); | ||
message.result = deserializer.predicate(); | ||
} | ||
var messageBody = dataDeserializer.fetchRawBytes( | ||
messageBodyLength, | ||
true, | ||
'message_data' | ||
); | ||
waitMessage = this.messagesWaitResponse.get(message.req_msg_id); | ||
var paddingLength = totalLength - dataDeserializer.getOffset(); | ||
if (paddingLength < 12 || paddingLength > 1024) { | ||
throw new Error('[MT] Invalid padding length: ' + paddingLength); | ||
} | ||
waitMessage.resolve(message.result); | ||
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]'); | ||
this.messagesWaitResponse.delete(message.req_msg_id); | ||
return; | ||
var offset = this.getOffset(); | ||
default: | ||
return; | ||
} | ||
} | ||
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]'); | ||
// TODO: Use debounce | ||
sendAcks() { | ||
if (!this.pendingAcks.length) { | ||
return; | ||
} | ||
var sentMessage = self.sentMessages[result.req_msg_id]; | ||
var type = (sentMessage && sentMessage.resultType) || 'Object'; | ||
const serializer = new TLSerializer({ mtproto: true }); | ||
serializer.predicate( | ||
{ | ||
_: 'msgs_ack', | ||
msg_ids: this.pendingAcks, | ||
}, | ||
'MsgsAck' | ||
); | ||
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'); | ||
this.pendingAcks = []; | ||
return { | ||
response, | ||
messageId, | ||
sessionId: this.sessionId, | ||
seqNo, | ||
messageDeferred: message.deferred.promise, | ||
}; | ||
}); | ||
this.sendEncryptedMessage(serializer, { isContentRelated: false }); | ||
} | ||
sendPlainRequest(requestBuffer) { | ||
const requestLength = requestBuffer.byteLength; | ||
const requestArray = new Int32Array(requestBuffer); | ||
ackMessage(messageId) { | ||
this.pendingAcks.push(messageId); | ||
const header = new TLSerialization(); | ||
header.storeLongP(0, 0, 'auth_key_id'); | ||
header.storeLong(this.generateMessageId(), 'msg_id'); | ||
header.storeInt(requestLength, 'request_length'); | ||
this.sendAcks(); | ||
} | ||
const headerBuffer = header.getBuffer(); | ||
const headerArray = new Int32Array(headerBuffer); | ||
const headerLength = headerBuffer.byteLength; | ||
call(method, params = {}) { | ||
if (!this.storage.pGet('authKey')) { | ||
return new Promise((resolve, reject) => { | ||
this.messagesWaitAuth.push({ method, params, resolve, reject }); | ||
}); | ||
} | ||
const resultBuffer = new ArrayBuffer(headerLength + requestLength); | ||
const resultArray = new Int32Array(resultBuffer); | ||
const serializer = new TLSerializer(); | ||
resultArray.set(headerArray); | ||
resultArray.set(requestArray, headerArray.length); | ||
serializer.method('invokeWithLayer', { | ||
layer: 108, | ||
}); | ||
const requestData = resultArray; | ||
serializer.method('initConnection', { | ||
flags: 0, // because the proxy is not set | ||
api_id: this.api_id, | ||
device_model: navigator.userAgent || 'Unknown UserAgent', | ||
system_version: navigator.platform || 'Unknown Platform', | ||
app_version: '1.0.0', | ||
system_lang_code: navigator.language || 'en', | ||
lang_code: navigator.language || 'en', | ||
}); | ||
return http | ||
.post(this.url, requestData, { | ||
responseType: 'arraybuffer', | ||
transformRequest: null, | ||
}) | ||
.then(function(result) { | ||
if (!result.data || !result.data.byteLength) { | ||
throw new Error('no data'); | ||
} | ||
serializer.method(method, { | ||
api_hash: this.api_hash, // TODO: not found in scheme | ||
api_id: this.api_id, // TODO: not found in scheme | ||
...params, | ||
}); | ||
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 new Promise(async (resolve, reject) => { | ||
const messageId = await this.sendEncryptedMessage(serializer); | ||
const messageIdAsKey = longFromInts(messageId[0], messageId[1]); | ||
return deserializer; | ||
this.messagesWaitResponse.set(messageIdAsKey, { | ||
method, | ||
params, | ||
resolve, | ||
reject, | ||
}); | ||
}); | ||
} | ||
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, | ||
}; | ||
} | ||
// https://core.telegram.org/mtproto/description#schematic-presentation-of-messages | ||
// Encrypted Message: | ||
// 1. auth_key_id (int64) | ||
// 2. msg_key (int128) | ||
// 3. encrypted_data | ||
// encrypted_data: | ||
// 4. salt (int64) | ||
// 5. session_id (int64) | ||
// 6. message_id (int64) | ||
// 7. seq_no (int32) | ||
// 8. message_data_length (int32) | ||
// 9. message_data | ||
// 10. padding 12..1024 | ||
async sendEncryptedMessage(messageSerializer, options = {}) { | ||
const { isContentRelated = true } = options; | ||
getDecryptedMessage(authKeyUint8, msgKey, encryptedData) { | ||
const keyIv = getAesKeyIv(authKeyUint8, msgKey, false); | ||
return convertToArrayBuffer( | ||
aesDecryptSync(encryptedData, keyIv[0], keyIv[1]) | ||
); | ||
} | ||
const messageData = messageSerializer.getBytes(); | ||
const authKey = this.storage.pGetBytes('authKey'); | ||
const serverSalt = this.storage.pGetBytes('serverSalt'); | ||
const messageId = this.getMessageId(); | ||
const seqNo = this.getSeqNo(isContentRelated); | ||
const minPadding = 12; | ||
const unpadded = (32 + messageData.length + minPadding) % 16; | ||
const padding = minPadding + (unpadded ? 16 - unpadded : 0); | ||
processMessage(message, messageId) { | ||
// console.log('processMessage', message, messageId); | ||
let sentMessage; | ||
const plainDataSerializer = new TLSerializer(); | ||
plainDataSerializer.bytesRaw(serverSalt); | ||
plainDataSerializer.bytesRaw(this.sessionId); | ||
plainDataSerializer.long(messageId); | ||
plainDataSerializer.int(seqNo); | ||
plainDataSerializer.uint32(messageData.length); | ||
plainDataSerializer.bytesRaw(messageData); | ||
plainDataSerializer.bytesRaw(getRandomBytes(padding)); | ||
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; | ||
const plainData = plainDataSerializer.getBytes(); | ||
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'); | ||
} | ||
const messageKeyLarge = await SHA256( | ||
concatBytes(authKey.slice(88, 120), plainData) | ||
); | ||
const messageKey = messageKeyLarge.slice(8, 24); | ||
const encryptedData = ( | ||
await this.getAESInstance(authKey, messageKey, false) | ||
).encrypt(plainData); | ||
this.setServerSalt(longToBytes(message.new_server_salt)); | ||
this.sendEncryptedRequest(this.sentMessages[message.bad_msg_id]); | ||
this.ackMessage(messageId); | ||
break; | ||
const authKeyId = (await SHA1(authKey)).slice(12, 20); | ||
const serializer = new TLSerializer(); | ||
serializer.setAbridgedHeader( | ||
authKeyId.length + messageKey.length + encryptedData.length | ||
); | ||
serializer.bytesRaw(authKeyId); | ||
serializer.bytesRaw(messageKey); | ||
serializer.bytesRaw(encryptedData); | ||
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'); | ||
} | ||
this.sendMessage(serializer.getBytes()); | ||
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; | ||
return messageId; | ||
} | ||
case 'message': | ||
this.ackMessage(messageId); | ||
this.processMessage(message.body, message.msg_id); | ||
break; | ||
sendPlainMessage(method, params) { | ||
const serializer = new TLSerializer({ mtproto: true }); | ||
serializer.method(method, params); | ||
case 'new_session_created': | ||
this.ackMessage(messageId); | ||
const requestBuffer = serializer.getBuffer(); | ||
const requestLength = requestBuffer.byteLength; | ||
const requestBytes = new Uint8Array(requestBuffer); | ||
this.processMessageAck(message.first_msg_id); | ||
this.setServerSalt(longToBytes(message.server_salt)); | ||
const header = new TLSerializer(); | ||
header.bytesRaw(new Uint8Array([(requestLength + 20) / 4])); | ||
header.long([0, 0]); // auth_key_id (8) | ||
header.long(this.getMessageId()); // msg_id (8) | ||
header.uint32(requestLength); // request_length (4) | ||
break; | ||
const headerBuffer = header.getBuffer(); | ||
const headerArray = new Uint8Array(headerBuffer); | ||
const headerLength = headerBuffer.byteLength; | ||
case 'msgs_ack': | ||
for (var i = 0; i < message.msg_ids.length; i++) { | ||
this.processMessageAck(message.msg_ids[i]); | ||
} | ||
break; | ||
const resultBuffer = new ArrayBuffer(headerLength + requestLength); | ||
const resultBytes = new Uint8Array(resultBuffer); | ||
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 */ | ||
resultBytes.set(headerArray); | ||
resultBytes.set(requestBytes, headerArray.length); | ||
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; | ||
} | ||
this.sendMessage(resultBytes); | ||
} | ||
ackMessage(messageId) { | ||
// console.log('ackMessage[messageId]:', messageId); | ||
this.pendingAcks.push(messageId); | ||
async sendMessage(bytes) { | ||
this.socket.send(await this.obfuscate(bytes)); | ||
} | ||
processMessageAck(messageId) { | ||
const sentMessage = this.sentMessages[messageId]; | ||
if (sentMessage && !sentMessage.acked) { | ||
delete sentMessage.body; | ||
sentMessage.acked = true; | ||
async generateObfuscationKeys() { | ||
const protocolId = 0xefefefef; | ||
const init1bytes = getRandomBytes(64); | ||
const init1buffer = init1bytes.buffer; | ||
const init1data = new DataView(init1buffer); | ||
init1data.setUint32(56, protocolId, true); | ||
return true; | ||
const init2buffer = new ArrayBuffer(init1buffer.byteLength); | ||
const init2bytes = new Uint8Array(init2buffer); | ||
for (let i = 0; i < init2buffer.byteLength; i++) { | ||
init2bytes[init2buffer.byteLength - i - 1] = init1bytes[i]; | ||
} | ||
return false; | ||
} | ||
let encryptKey = new Uint8Array(init1buffer, 8, 32); | ||
const encryptIV = new Uint8Array(16); | ||
encryptIV.set(new Uint8Array(init1buffer, 40, 16)); | ||
applyServerTime(serverTime) { | ||
const newTimeOffset = serverTime - Math.floor(this.localTime / 1000); | ||
const changed = Math.abs(this.timeOffset - newTimeOffset) > 10; | ||
let decryptKey = new Uint8Array(init2buffer, 8, 32); | ||
const decryptIV = new Uint8Array(16); | ||
decryptIV.set(new Uint8Array(init2buffer, 40, 16)); | ||
this.lastMessageId = [0, 0]; | ||
this.timeOffset = newTimeOffset; | ||
this.encryptAES = new AES.CTR(encryptKey, encryptIV); | ||
this.decryptAES = new AES.CTR(decryptKey, decryptIV); | ||
console.log( | ||
'Apply server time', | ||
serverTime, | ||
this.localTime, | ||
newTimeOffset, | ||
changed | ||
); | ||
const init3buffer = await this.obfuscate(init1bytes); | ||
init1bytes.set(new Uint8Array(init3buffer, 56, 8), 56); | ||
return changed; | ||
return init1bytes; | ||
} | ||
updateSession() { | ||
this.prevSessionId = this.sessionId; | ||
this.sessionId = new Array(8); | ||
secureRandom.nextBytes(this.sessionId); | ||
this._seqNo = 0; | ||
async obfuscate(bytes) { | ||
return this.encryptAES.encrypt(bytes).buffer; | ||
} | ||
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); | ||
async deobfuscate(bytes) { | ||
return this.decryptAES.decrypt(bytes).buffer; | ||
} | ||
generateSeqNo(notContentRelated) { | ||
var seqNo = this._seqNo * 2; | ||
getMessageId() { | ||
const timeOffset = this.storage.get('timeOffset'); | ||
if (!notContentRelated) { | ||
seqNo += 1; | ||
this._seqNo += 1; | ||
} | ||
return seqNo; | ||
} | ||
generateMessageId() { | ||
const timeTicks = tsNow(); | ||
const timeSec = Math.floor(timeTicks / 1000) + this.timeOffset; | ||
const timeTicks = Date.now(); | ||
const timeSec = Math.floor(timeTicks / 1000) + timeOffset; | ||
const timeMSec = timeTicks % 1000; | ||
const random = nextRandomInt(0xffff); | ||
const random = getRandomInt(0xffff); | ||
@@ -997,58 +637,54 @@ const { lastMessageId } = this; | ||
return longFromInts(messageId[0], messageId[1]); | ||
return messageId; | ||
} | ||
getServerSalt() { | ||
const key = `dc${this.dcId}ServerSalt`; | ||
getSeqNo(isContentRelated = true) { | ||
let seqNo = this.seqNo * 2; | ||
const fromThis = this[key]; | ||
if (fromThis) { | ||
return fromThis; | ||
if (isContentRelated) { | ||
seqNo += 1; | ||
this.seqNo += 1; | ||
} | ||
const fromStorage = localStorage.getItem(key); | ||
if (fromStorage) { | ||
return JSON.parse(fromStorage); | ||
} | ||
return null; | ||
return seqNo; | ||
} | ||
setServerSalt(serverSalt) { | ||
const key = `dc${this.dcId}ServerSalt`; | ||
this[key] = serverSalt; | ||
localStorage.setItem(key, JSON.stringify(serverSalt)); | ||
updateSession() { | ||
this.seqNo = 0; | ||
this.sessionId = getRandomBytes(8); | ||
this.lastMessageId = [0, 0]; | ||
} | ||
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; | ||
async getAESInstance(authKey, messageKey, isServer) { | ||
const x = isServer ? 8 : 0; | ||
const sha256a = await SHA256( | ||
concatBytes(messageKey, authKey.slice(x, 36 + x)) | ||
); | ||
const sha256b = await SHA256( | ||
concatBytes(authKey.slice(40 + x, 76 + x), messageKey) | ||
); | ||
const aesKey = concatBytes( | ||
sha256a.slice(0, 8), | ||
sha256b.slice(8, 24), | ||
sha256a.slice(24, 32) | ||
); | ||
const aesIV = concatBytes( | ||
sha256b.slice(0, 8), | ||
sha256a.slice(8, 24), | ||
sha256b.slice(24, 32) | ||
); | ||
return new AES.IGE(aesKey, aesIV); | ||
} | ||
setAuthKey(authKey) { | ||
const key = `dc${this.dcId}AuthKey`; | ||
this[key] = authKey; | ||
localStorage.setItem(key, JSON.stringify(authKey)); | ||
changeDc(dcId) { | ||
// TODO: Add import/export auth | ||
this.setDc(dcId); | ||
this.recconect(); | ||
} | ||
setDc(dcId) { | ||
// TODO: Use this.storage for save dcId | ||
const fromStorage = localStorage.getItem('dcId', dcId); | ||
this.dcId = dcId || fromStorage || 2; | ||
this.dcId = dcId || fromStorage || defaultDC; | ||
this.storage.setPrefix(this.dcId); | ||
localStorage.setItem('dcId', this.dcId); | ||
@@ -1064,176 +700,30 @@ | ||
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 ? '/apiws_test' : '/apiws'; | ||
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}`; | ||
} | ||
this.url = `wss://${subdomainsMap[this.dcId]}.web.telegram.org${urlPath}`; | ||
} | ||
runLongPoll() { | ||
if (this.longPollRunning) { | ||
return; | ||
} | ||
this.longPollRunning = true; | ||
connect() { | ||
this.socket = new WebSocket(this.url, 'binary'); | ||
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(); | ||
this.socket.addEventListener('error', this.handleWSError); | ||
this.socket.addEventListener('open', this.handleWSOpen); | ||
this.socket.addEventListener('close', this.handleWSClose); | ||
this.socket.addEventListener('message', this.handleWSMessage); | ||
} | ||
getApiCallMessage(method, params = {}) { | ||
const serializer = new TLSerialization(); | ||
recconect() { | ||
this.socket.removeEventListener('error', this.handleWSError); | ||
this.socket.removeEventListener('open', this.handleWSOpen); | ||
this.socket.removeEventListener('close', this.handleWSClose); | ||
this.socket.removeEventListener('message', this.handleWSMessage); | ||
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'); | ||
if (this.socket.readyState === 1) { | ||
this.socket.close(); | ||
} | ||
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; | ||
this.connect(); | ||
} | ||
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 { | ||
constructor({ api_id, api_hash, test = false, https = false }) { | ||
this.api = new API({ api_id, api_hash, test, https }); | ||
} | ||
} | ||
module.exports = MTProto; |
@@ -23,5 +23,6 @@ const { Zlib } = require('zlibjs/bin/gunzip.min.js'); | ||
const { BigInteger, SecureRandom } = require('../vendors/jsbn'); | ||
const { PBKDF2, SHA256 } = require('./crypto'); | ||
function bigIntToBytes(bigInt, len) { | ||
return hexToBytes(bigInt.toString(16), len); | ||
function bigIntToBytes(bigInt, length) { | ||
return hexToBytes(bigInt.toString(16), length); | ||
} | ||
@@ -52,4 +53,4 @@ | ||
function randomBytes(len) { | ||
const bytes = new Uint8Array(len); | ||
function getRandomBytes(length) { | ||
const bytes = new Uint8Array(length); | ||
crypto.getRandomValues(bytes); | ||
@@ -81,3 +82,3 @@ return bytes; | ||
if (typeof bytes === 'number') { | ||
merged.set(randomBytes(totalLength - offset), offset); | ||
merged.set(getRandomBytes(totalLength - offset), offset); | ||
} else { | ||
@@ -94,27 +95,2 @@ merged.set( | ||
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 }) { | ||
@@ -140,3 +116,3 @@ const H = SHA256; | ||
const pBigInt = bytesToBigInt(p); | ||
const aBigInt = bytesToBigInt(randomBytes(256)); | ||
const aBigInt = bytesToBigInt(getRandomBytes(256)); | ||
const gABigInt = gBigInt.modPow(aBigInt, pBigInt); | ||
@@ -499,14 +475,12 @@ const gABytes = bigIntToBytes(gABigInt); | ||
function longFromInts(high, low) { | ||
return bigint(high) | ||
return bigInt(high) | ||
.shiftLeft(32) | ||
.add(bigint(low)) | ||
.add(bigInt(low)) | ||
.toString(10); | ||
} | ||
function intToUint(val) { | ||
val = parseInt(val); | ||
if (val < 0) { | ||
val = val + 4294967296; | ||
} | ||
return val; | ||
function intToUint(value) { | ||
value = +value; | ||
return value < 0 ? value + 4294967296 : value; | ||
} | ||
@@ -546,13 +520,8 @@ | ||
function rsaEncrypt(publicKey, bytes) { | ||
bytes = addPadding(bytes, 255); | ||
const encryptedBigInt = bytesToBigInt(bytes).modPow( | ||
bigInt(publicKey.exponent, 16), | ||
bigInt(publicKey.modulus, 16) | ||
); | ||
// console.log('RSA encrypt start') | ||
var N = new BigInteger(publicKey.modulus, 16); | ||
var E = new BigInteger(publicKey.exponent, 16); | ||
var X = new BigInteger(bytes); | ||
var encryptedBigInt = X.modPowInt(E, N), | ||
encryptedBytes = bytesFromBigInt(encryptedBigInt, 256); | ||
// console.log('RSA encrypt finish') | ||
return encryptedBytes; | ||
return bigIntToBytes(encryptedBigInt, 256); | ||
} | ||
@@ -631,3 +600,3 @@ | ||
function nextRandomInt(maxValue) { | ||
function getRandomInt(maxValue) { | ||
return Math.floor(Math.random() * maxValue); | ||
@@ -676,4 +645,4 @@ } | ||
for (var i = 0; i < 3; i++) { | ||
var q = (nextRandomInt(128) & 15) + 17; | ||
var x = bigint(nextRandomInt(1000000000) + 1); | ||
var q = (getRandomInt(128) & 15) + 17; | ||
var x = bigint(getRandomInt(1000000000) + 1); | ||
var y = x.clone(); | ||
@@ -753,4 +722,4 @@ var lim = 1 << (i + 18); | ||
for (var i = 0; i < 3; i++) { | ||
var q = goog.math.Long.fromInt((nextRandomInt(128) & 15) + 17); | ||
var x = goog.math.Long.fromInt(nextRandomInt(1000000000) + 1); | ||
var q = goog.math.Long.fromInt((getRandomInt(128) & 15) + 17); | ||
var x = goog.math.Long.fromInt(getRandomInt(1000000000) + 1); | ||
var y = x; | ||
@@ -826,4 +795,4 @@ var lim = 1 << (i + 18); | ||
for (i = 0; i < 3; i++) { | ||
q = (nextRandomInt(128) & 15) + 17; | ||
copyInt_(x, nextRandomInt(1000000000) + 1); | ||
q = (getRandomInt(128) & 15) + 17; | ||
copyInt_(x, getRandomInt(1000000000) + 1); | ||
copy_(y, x); | ||
@@ -909,3 +878,3 @@ lim = 1 << (i + 18); | ||
for (var i = 0; i < 16; i++) { | ||
nonce.push(nextRandomInt(0xff)); | ||
nonce.push(getRandomInt(0xff)); | ||
} | ||
@@ -952,7 +921,5 @@ return nonce; | ||
bytesToBigInt, | ||
randomBytes, | ||
getRandomBytes, | ||
xorBytes, | ||
concatBytes, | ||
SHA256, | ||
PBKDF2, | ||
getSRPParams, | ||
@@ -996,3 +963,3 @@ bigint, | ||
gzipUncompress, | ||
nextRandomInt, | ||
getRandomInt, | ||
pqPrimeFactorization, | ||
@@ -999,0 +966,0 @@ pqPrimeBigInteger, |
@@ -1,87 +0,80 @@ | ||
const { TLSerialization } = require('../tl'); | ||
const { | ||
bigStringInt, | ||
bytesToHex, | ||
bytesFromHex, | ||
sha1BytesSync, | ||
} = require('../utils'); | ||
const bigInt = require('big-integer'); | ||
const TLSerializer = require('../tl/serializer'); | ||
const { bytesToHex, bytesFromHex } = require('../utils'); | ||
const { SHA1 } = require('../utils/crypto'); | ||
const RsaKeysManager = (function() { | ||
var publisKeysHex = [ | ||
{ | ||
modulus: | ||
'c150023e2f70db7985ded064759cfecf0af328e69a41daf4d6f01b538135a6f91f8f8b2a0ec9ba9720ce352efcf6c5680ffc424bd634864902de0b4bd6d49f4e580230e3ae97d95c8b19442b3c0a10d8f5633fecedd6926a7f6dab0ddb7d457f9ea81b8465fcd6fffeed114011df91c059caedaf97625f6c96ecc74725556934ef781d866b34f011fce4d835a090196e9a5f0e4449af7eb697ddb9076494ca5f81104a305b6dd27665722c46b60e5df680fb16b210607ef217652e60236c255f6a28315f4083a96791d7214bf64c1df4fd0db1944fb26a2a57031b32eee64ad15a8ba68885cde74a5bfc920f6abf59ba5c75506373e7130f9042da922179251f', | ||
exponent: '010001', | ||
}, | ||
{ | ||
modulus: | ||
'aeec36c8ffc109cb099624685b97815415657bd76d8c9c3e398103d7ad16c9bba6f525ed0412d7ae2c2de2b44e77d72cbf4b7438709a4e646a05c43427c7f184debf72947519680e651500890c6832796dd11f772c25ff8f576755afe055b0a3752c696eb7d8da0d8be1faf38c9bdd97ce0a77d3916230c4032167100edd0f9e7a3a9b602d04367b689536af0d64b613ccba7962939d3b57682beb6dae5b608130b2e52aca78ba023cf6ce806b1dc49c72cf928a7199d22e3d7ac84e47bc9427d0236945d10dbd15177bab413fbf0edfda09f014c7a7da088dde9759702ca760af2b8e4e97cc055c617bd74c3d97008635b98dc4d621b4891da9fb0473047927', | ||
exponent: '010001', | ||
}, | ||
{ | ||
modulus: | ||
'bdf2c77d81f6afd47bd30f29ac76e55adfe70e487e5e48297e5a9055c9c07d2b93b4ed3994d3eca5098bf18d978d54f8b7c713eb10247607e69af9ef44f38e28f8b439f257a11572945cc0406fe3f37bb92b79112db69eedf2dc71584a661638ea5becb9e23585074b80d57d9f5710dd30d2da940e0ada2f1b878397dc1a72b5ce2531b6f7dd158e09c828d03450ca0ff8a174deacebcaa22dde84ef66ad370f259d18af806638012da0ca4a70baa83d9c158f3552bc9158e69bf332a45809e1c36905a5caa12348dd57941a482131be7b2355a5f4635374f3bd3ddf5ff925bf4809ee27c1e67d9120c5fe08a9de458b1b4a3c5d0a428437f2beca81f4e2d5ff', | ||
exponent: '010001', | ||
}, | ||
{ | ||
modulus: | ||
'b3f762b739be98f343eb1921cf0148cfa27ff7af02b6471213fed9daa0098976e667750324f1abcea4c31e43b7d11f1579133f2b3d9fe27474e462058884e5e1b123be9cbbc6a443b2925c08520e7325e6f1a6d50e117eb61ea49d2534c8bb4d2ae4153fabe832b9edf4c5755fdd8b19940b81d1d96cf433d19e6a22968a85dc80f0312f596bd2530c1cfb28b5fe019ac9bc25cd9c2a5d8a0f3a1c0c79bcca524d315b5e21b5c26b46babe3d75d06d1cd33329ec782a0f22891ed1db42a1d6c0dea431428bc4d7aabdcf3e0eb6fda4e23eb7733e7727e9a1915580796c55188d2596d2665ad1182ba7abf15aaa5a8b779ea996317a20ae044b820bff35b6e8a1', | ||
exponent: '010001', | ||
}, | ||
{ | ||
modulus: | ||
'be6a71558ee577ff03023cfa17aab4e6c86383cff8a7ad38edb9fafe6f323f2d5106cbc8cafb83b869cffd1ccf121cd743d509e589e68765c96601e813dc5b9dfc4be415c7a6526132d0035ca33d6d6075d4f535122a1cdfe017041f1088d1419f65c8e5490ee613e16dbf662698c0f54870f0475fa893fc41eb55b08ff1ac211bc045ded31be27d12c96d8d3cfc6a7ae8aa50bf2ee0f30ed507cc2581e3dec56de94f5dc0a7abee0be990b893f2887bd2c6310a1e0a9e3e38bd34fded2541508dc102a9c9b4c95effd9dd2dfe96c29be647d6c69d66ca500843cfaed6e440196f1dbe0e2e22163c61ca48c79116fa77216726749a976a1c4b0944b5121e8c01', | ||
exponent: '010001', | ||
}, | ||
]; | ||
const publisKeys = [ | ||
{ | ||
modulus: | ||
'c150023e2f70db7985ded064759cfecf0af328e69a41daf4d6f01b538135a6f91f8f8b2a0ec9ba9720ce352efcf6c5680ffc424bd634864902de0b4bd6d49f4e580230e3ae97d95c8b19442b3c0a10d8f5633fecedd6926a7f6dab0ddb7d457f9ea81b8465fcd6fffeed114011df91c059caedaf97625f6c96ecc74725556934ef781d866b34f011fce4d835a090196e9a5f0e4449af7eb697ddb9076494ca5f81104a305b6dd27665722c46b60e5df680fb16b210607ef217652e60236c255f6a28315f4083a96791d7214bf64c1df4fd0db1944fb26a2a57031b32eee64ad15a8ba68885cde74a5bfc920f6abf59ba5c75506373e7130f9042da922179251f', | ||
exponent: '010001', | ||
}, | ||
{ | ||
modulus: | ||
'aeec36c8ffc109cb099624685b97815415657bd76d8c9c3e398103d7ad16c9bba6f525ed0412d7ae2c2de2b44e77d72cbf4b7438709a4e646a05c43427c7f184debf72947519680e651500890c6832796dd11f772c25ff8f576755afe055b0a3752c696eb7d8da0d8be1faf38c9bdd97ce0a77d3916230c4032167100edd0f9e7a3a9b602d04367b689536af0d64b613ccba7962939d3b57682beb6dae5b608130b2e52aca78ba023cf6ce806b1dc49c72cf928a7199d22e3d7ac84e47bc9427d0236945d10dbd15177bab413fbf0edfda09f014c7a7da088dde9759702ca760af2b8e4e97cc055c617bd74c3d97008635b98dc4d621b4891da9fb0473047927', | ||
exponent: '010001', | ||
}, | ||
{ | ||
modulus: | ||
'bdf2c77d81f6afd47bd30f29ac76e55adfe70e487e5e48297e5a9055c9c07d2b93b4ed3994d3eca5098bf18d978d54f8b7c713eb10247607e69af9ef44f38e28f8b439f257a11572945cc0406fe3f37bb92b79112db69eedf2dc71584a661638ea5becb9e23585074b80d57d9f5710dd30d2da940e0ada2f1b878397dc1a72b5ce2531b6f7dd158e09c828d03450ca0ff8a174deacebcaa22dde84ef66ad370f259d18af806638012da0ca4a70baa83d9c158f3552bc9158e69bf332a45809e1c36905a5caa12348dd57941a482131be7b2355a5f4635374f3bd3ddf5ff925bf4809ee27c1e67d9120c5fe08a9de458b1b4a3c5d0a428437f2beca81f4e2d5ff', | ||
exponent: '010001', | ||
}, | ||
{ | ||
modulus: | ||
'b3f762b739be98f343eb1921cf0148cfa27ff7af02b6471213fed9daa0098976e667750324f1abcea4c31e43b7d11f1579133f2b3d9fe27474e462058884e5e1b123be9cbbc6a443b2925c08520e7325e6f1a6d50e117eb61ea49d2534c8bb4d2ae4153fabe832b9edf4c5755fdd8b19940b81d1d96cf433d19e6a22968a85dc80f0312f596bd2530c1cfb28b5fe019ac9bc25cd9c2a5d8a0f3a1c0c79bcca524d315b5e21b5c26b46babe3d75d06d1cd33329ec782a0f22891ed1db42a1d6c0dea431428bc4d7aabdcf3e0eb6fda4e23eb7733e7727e9a1915580796c55188d2596d2665ad1182ba7abf15aaa5a8b779ea996317a20ae044b820bff35b6e8a1', | ||
exponent: '010001', | ||
}, | ||
{ | ||
modulus: | ||
'be6a71558ee577ff03023cfa17aab4e6c86383cff8a7ad38edb9fafe6f323f2d5106cbc8cafb83b869cffd1ccf121cd743d509e589e68765c96601e813dc5b9dfc4be415c7a6526132d0035ca33d6d6075d4f535122a1cdfe017041f1088d1419f65c8e5490ee613e16dbf662698c0f54870f0475fa893fc41eb55b08ff1ac211bc045ded31be27d12c96d8d3cfc6a7ae8aa50bf2ee0f30ed507cc2581e3dec56de94f5dc0a7abee0be990b893f2887bd2c6310a1e0a9e3e38bd34fded2541508dc102a9c9b4c95effd9dd2dfe96c29be647d6c69d66ca500843cfaed6e440196f1dbe0e2e22163c61ca48c79116fa77216726749a976a1c4b0944b5121e8c01', | ||
exponent: '010001', | ||
}, | ||
]; | ||
var publicKeysParsed = {}; | ||
var prepared = false; | ||
async function getPublisKeysByHex() { | ||
const publisKeysByHex = {}; | ||
function prepareRsaKeys() { | ||
if (prepared) { | ||
return; | ||
} | ||
for (const publisKey of publisKeys) { | ||
const RSAPublicKey = new TLSerializer(); | ||
RSAPublicKey.bytes(bytesFromHex(publisKey.modulus), 'n'); | ||
RSAPublicKey.bytes(bytesFromHex(publisKey.exponent), 'e'); | ||
for (var i = 0; i < publisKeysHex.length; i++) { | ||
var keyParsed = publisKeysHex[i]; | ||
const buffer = RSAPublicKey.getBuffer(); | ||
var RSAPublicKey = new TLSerialization(); | ||
RSAPublicKey.storeBytes(bytesFromHex(keyParsed.modulus), 'n'); | ||
RSAPublicKey.storeBytes(bytesFromHex(keyParsed.exponent), 'e'); | ||
const fingerprintBytes = (await SHA1(buffer)).slice(-8); | ||
fingerprintBytes.reverse(); | ||
var buffer = RSAPublicKey.getBuffer(); | ||
publisKeysByHex[bytesToHex(fingerprintBytes)] = { | ||
modulus: publisKey.modulus, | ||
exponent: publisKey.exponent, | ||
}; | ||
} | ||
var fingerprintBytes = sha1BytesSync(buffer).slice(-8); | ||
fingerprintBytes.reverse(); | ||
return publisKeysByHex; | ||
} | ||
publicKeysParsed[bytesToHex(fingerprintBytes)] = { | ||
modulus: keyParsed.modulus, | ||
exponent: keyParsed.exponent, | ||
}; | ||
async function getRsaKeyByFingerprints(fingerprints) { | ||
let resultKey = null; | ||
const publisKeysByHex = await getPublisKeysByHex(); | ||
fingerprints.forEach(fingerprint => { | ||
if (!!resultKey) { | ||
return; | ||
} | ||
prepared = true; | ||
} | ||
const fingerprintHex = bigInt(fingerprint).toString(16); | ||
function selectRsaKeyByFingerPrint(fingerprints) { | ||
prepareRsaKeys(); | ||
const foundKey = publisKeysByHex[fingerprintHex]; | ||
var fingerprintHex, foundKey, i; | ||
for (i = 0; i < fingerprints.length; i++) { | ||
fingerprintHex = bigStringInt(fingerprints[i]).toString(16); | ||
if ((foundKey = publicKeysParsed[fingerprintHex])) { | ||
return { ...{ fingerprint: fingerprints[i] }, ...foundKey }; | ||
} | ||
if (foundKey) { | ||
resultKey = { | ||
fingerprint, | ||
...foundKey, | ||
}; | ||
} | ||
}); | ||
return false; | ||
} | ||
return resultKey; | ||
} | ||
return { | ||
prepare: prepareRsaKeys, | ||
select: selectRsaKeyByFingerPrint, | ||
}; | ||
})(); | ||
module.exports = RsaKeysManager; | ||
module.exports = { getRsaKeyByFingerprints }; |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
525471
18
18041
0
100
11
+ Addedaes-js@3.1.2
+ Addedaes-js@3.1.2(transitive)
- Removedaxios@0.19.0
- Removedaxios@0.19.0(transitive)
- Removedfollow-redirects@1.5.10(transitive)
- Removedis-buffer@2.0.5(transitive)