@mtproto/core
Advanced tools
Comparing version 0.0.7 to 0.0.8
{ | ||
"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); | ||
}); | ||
}); |
1230
src/main.js
@@ -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, |
485805
16825
11
7
5
15
+ Addedbig-integer@1.6.48
+ Addedbig-integer@1.6.48(transitive)