UNPKG

@tgsnake/core

Version:

Pure Telegram MTProto library for nodejs

528 lines (527 loc) 25.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SecretChat = void 0; const index_js_1 = require("../../raw/index.js"); const index_js_2 = require("../../storage/index.js"); const platform_node_js_1 = require("../../platform.node.js"); const Logger_js_1 = require("../../Logger.js"); const index_js_3 = require("../../errors/index.js"); const index_js_4 = require("../../crypto/index.js"); const helpers_js_1 = require("../../helpers.js"); function sha1(data) { const hash = platform_node_js_1.crypto.createHash('sha1'); hash.update(data); return hash.digest(); } class SecretChat { _storage; _client; _dhConfig; _mutex; _tempAuthKey; _waiting; constructor(storage, client) { this._storage = storage; this._client = client; this._mutex = new platform_node_js_1.Mutex(); this._tempAuthKey = new Map(); this._waiting = []; } async reqDHConfig() { const release = await this._mutex.acquire(); try { let version = 0; if (this._dhConfig) { if (this._dhConfig instanceof index_js_1.Raw.messages.DhConfig) { version = this._dhConfig.version; } } const dh = await this._client.invoke(new index_js_1.Raw.messages.GetDhConfig({ randomLength: 0, version: version, })); if (dh instanceof index_js_1.Raw.messages.DhConfigNotModified) { return this._dhConfig; } this._dhConfig = dh; return dh; } finally { release(); } } async start(userId) { Logger_js_1.Logger.debug(`[127] starting secret chat for ${userId}`); const peer = await this._client.resolvePeer(userId); const dh = await this.reqDHConfig(); const p = await (0, helpers_js_1.bufferToBigint)(dh.p, false); const a = await (0, helpers_js_1.bufferToBigint)(platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(256)), false); const gA = (0, helpers_js_1.bigIntPow)(BigInt(dh.g), a, p); index_js_3.SecurityCheckMismatch.check(BigInt(1) < gA && gA < p - BigInt(1), 'gA must be greater than one and smaller than p-1'); index_js_3.SecurityCheckMismatch.check(BigInt(2) ** BigInt(2048 - 64) < gA && gA < p - BigInt(2) ** BigInt(2048 - 64), 'gA must be greater than 2^{2048 - 64} and smaller than p-2^{2048 -64}'); Logger_js_1.Logger.debug('[111] gA validation: OK'); const res = await this._client.invoke(new index_js_1.Raw.messages.RequestEncryption({ userId: peer, gA: await (0, helpers_js_1.bigintToBuffer)(gA, 256, false), randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(4)).readInt32LE(), })); const release = await this._mutex.acquire(); try { await index_js_2.SecretChat.save(this._storage, { id: res.id, accessHash: BigInt(0), authKey: await (0, helpers_js_1.bigintToBuffer)(a, 256, false), isAdmin: false, }); } finally { release(); } this._waiting.push(res.id); return res; } async accept(request) { Logger_js_1.Logger.debug(`[125] accepting secret chat from ${request.id}`); if (request.id === 0) { throw new index_js_3.SecretChatError.AlreadyAccepted(); } const dh = await this.reqDHConfig(); const p = await (0, helpers_js_1.bufferToBigint)(dh.p, false); const b = await (0, helpers_js_1.bufferToBigint)(platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(256)), false); const gA = (0, helpers_js_1.bufferToBigint)(request.gA, false); const gB = (0, helpers_js_1.bigIntPow)(BigInt(dh.g), b, p); const authKey = await (0, helpers_js_1.bigintToBuffer)((0, helpers_js_1.bigIntPow)(gA, b, p), 256, false); const fingerprint = sha1(authKey).subarray(-8).readBigInt64LE(); index_js_3.SecurityCheckMismatch.check(BigInt(1) < gA && gA < p - BigInt(1), 'gA must be greater than one and smaller than p-1'); index_js_3.SecurityCheckMismatch.check(BigInt(2) ** BigInt(2048 - 64) < gA && gA < p - BigInt(2) ** BigInt(2048 - 64), 'gA must be greater than 2^{2048 - 64} and smaller than p-2^{2048 -64}'); index_js_3.SecurityCheckMismatch.check(BigInt(1) < gB && gB < p - BigInt(1), 'gB must be greater than one and smaller than p-1'); index_js_3.SecurityCheckMismatch.check(BigInt(2) ** BigInt(2048 - 64) < gB && gB < p - BigInt(2) ** BigInt(2048 - 64), 'gB must be greater than 2^{2048 - 64} and smaller than p-2^{2048 -64}'); Logger_js_1.Logger.debug('[126] gA and gB validation: OK'); const release = await this._mutex.acquire(); try { await index_js_2.SecretChat.save(this._storage, { id: request.id, accessHash: request.accessHash, authKey: authKey, isAdmin: false, }); } finally { release(); } const res = await this._client.invoke(new index_js_1.Raw.messages.AcceptEncryption({ peer: new index_js_1.Raw.InputEncryptedChat({ chatId: request.id, accessHash: request.accessHash, }), gB: await (0, helpers_js_1.bigintToBuffer)(gB, 256, false), keyFingerprint: fingerprint, })); await this.notifyLayer(request.id); return res; } async finish(chat) { Logger_js_1.Logger.debug(`[129] finishing creating secret chat ${chat.id}`); const dh = await this.reqDHConfig(); const p = await (0, helpers_js_1.bufferToBigint)(dh.p, false); const gAOrB = await (0, helpers_js_1.bufferToBigint)(chat.gAOrB, false); index_js_3.SecurityCheckMismatch.check(BigInt(1) < gAOrB && gAOrB < p - BigInt(1), 'gAOrB must be greater than one and smaller than p-1'); index_js_3.SecurityCheckMismatch.check(BigInt(2) ** BigInt(2048 - 64) < gAOrB && gAOrB < p - BigInt(2) ** BigInt(2048 - 64), 'gAOrB must be greater than 2^{2048 - 64} and smaller than p-2^{2048 -64}'); Logger_js_1.Logger.debug('[128] gAOrB validation: OK'); const peer = await this._storage.getSecretChatById(chat.id); if (!peer) { throw new index_js_3.SecretChatError.ChatNotFound(chat.id); } const a = await (0, helpers_js_1.bufferToBigint)(peer.authKey, false); const authKey = await (0, helpers_js_1.bigintToBuffer)((0, helpers_js_1.bigIntPow)(gAOrB, a, p), 256, false); const fingerprint = sha1(authKey).subarray(-8).readBigInt64LE(); if (fingerprint !== chat.keyFingerprint) { throw new index_js_3.SecretChatError.FingerprintMismatch(); } const release = await this._mutex.acquire(); try { await this._storage.removeSecretChatById(peer.id); await index_js_2.SecretChat.save(this._storage, { id: chat.id, accessHash: chat.accessHash, authKey: authKey, isAdmin: true, }); } finally { release(); } const index = this._waiting.findIndex((id) => id === chat.id); if (index >= 0) { this._waiting.splice(index, 1); } return this.notifyLayer(chat.id); } async notifyLayer(chatId) { Logger_js_1.Logger.debug(`[130] notify layer for ${chatId}`); const peer = await this._storage.getSecretChatById(chatId); if (!peer) { throw new index_js_3.SecretChatError.ChatNotFound(chatId); } if (peer.layer !== 8) { return this._client.invoke(new index_js_1.Raw.messages.SendEncryptedService({ peer: peer.input, randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), data: await this.encrypt(chatId, new index_js_1.Raw.DecryptedMessageService8({ randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), randomBytes: platform_node_js_1.crypto.randomBytes(15 + 4 * Math.floor(Math.random() * 2)), action: new index_js_1.Raw.DecryptedMessageActionNotifyLayer17({ layer: Math.min(peer.layer, index_js_1.Raw.Layer), }), })), })); } return; } async destroy(chatId) { Logger_js_1.Logger.debug(`[131] destroying secret chat ${chatId}`); const release = await this._mutex.acquire(); try { await this._storage.removeSecretChatById(chatId); } finally { release(); } Logger_js_1.Logger.debug(`[132] ${chatId} was removed from session`); try { await this._client.invoke(new index_js_1.Raw.messages.DiscardEncryption({ chatId: chatId, })); } catch (_error) { } Logger_js_1.Logger.debug(`[133] ${chatId} already destroyed`); return true; } async rekeying(chatId) { Logger_js_1.Logger.debug(`[114] re-keying ${chatId}: initiator`); const peer = await this._storage.getSecretChatById(chatId); if (!peer) { throw new index_js_3.SecretChatError.ChatNotFound(chatId); } const dh = await this.reqDHConfig(); const p = await (0, helpers_js_1.bufferToBigint)(dh.p, false); const a = await (0, helpers_js_1.bufferToBigint)(platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(256)), false); const gA = (0, helpers_js_1.bigIntPow)(BigInt(dh.g), a, p); let e = platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(64)).readBigInt64LE(); peer.rekeyStep = 1; peer.rekeyExchange = e; index_js_3.SecurityCheckMismatch.check(BigInt(1) < gA && gA < p - BigInt(1), 'gA must be greater than one and smaller than p-1'); index_js_3.SecurityCheckMismatch.check(BigInt(2) ** BigInt(2048 - 64) < gA && gA < p - BigInt(2) ** BigInt(2048 - 64), 'gA must be greater than 2^{2048 - 64} and smaller than p-2^{2048 -64}'); Logger_js_1.Logger.debug('[115] gA validation: OK'); const release = await this._mutex.acquire(); try { await peer.update(this._storage); this._tempAuthKey.set(e, await (0, helpers_js_1.bigintToBuffer)(a, 256, false)); } finally { release(); } return this._client.invoke(new index_js_1.Raw.messages.SendEncryptedService({ peer: peer.input, randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), data: await this.encrypt(chatId, new index_js_1.Raw.DecryptedMessageService17({ randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), action: new index_js_1.Raw.DecryptedMessageActionRequestKey20({ gA: await (0, helpers_js_1.bigintToBuffer)(gA, 256, false), exchangeId: e, }), })), })); } async acceptRekeying(chatId, action) { Logger_js_1.Logger.debug(`[116] re-keying ${chatId}: accepting`); const peer = await this._storage.getSecretChatById(chatId); if (!peer) { throw new index_js_3.SecretChatError.ChatNotFound(chatId); } if (peer.rekeyStep) { if (peer.rekeyExchange > action.exchangeId) { Logger_js_1.Logger.info(`[117] Aborting rekeying: received exchangeId smaller than our exchangeId`); return; } if (peer.rekeyExchange === action.exchangeId) { Logger_js_1.Logger.info(`[118] Aborting rekeying: received exchangeId equal with our exchangeId`); const release = await this._mutex.acquire(); try { peer.rekeyStep = 0; peer.rekeyExchange = BigInt(0); await peer.update(this._storage); } finally { release(); } return; } } const dh = await this.reqDHConfig(); const p = await (0, helpers_js_1.bufferToBigint)(dh.p, false); const b = await (0, helpers_js_1.bufferToBigint)(platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(256)), false); const gA = (0, helpers_js_1.bufferToBigint)(action.gA, false); const gB = (0, helpers_js_1.bigIntPow)(BigInt(dh.g), b, p); const authKey = await (0, helpers_js_1.bigintToBuffer)((0, helpers_js_1.bigIntPow)(gA, b, p), 256, false); const fingerprint = sha1(authKey).subarray(-8); index_js_3.SecurityCheckMismatch.check(BigInt(1) < gB && gB < p - BigInt(1), 'gB must be greater than one and smaller than p-1'); index_js_3.SecurityCheckMismatch.check(BigInt(2) ** BigInt(2048 - 64) < gB && gB < p - BigInt(2) ** BigInt(2048 - 64), 'gB must be greater than 2^{2048 - 64} and smaller than p-2^{2048 -64}'); Logger_js_1.Logger.debug('[119] gB validation: OK'); const release = await this._mutex.acquire(); try { this._tempAuthKey.set(action.exchangeId, authKey); peer.rekeyStep = 2; peer.rekeyExchange = action.exchangeId; await peer.update(this._storage); } finally { release(); } return this._client.invoke(new index_js_1.Raw.messages.SendEncryptedService({ peer: peer.input, randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), data: await this.encrypt(chatId, new index_js_1.Raw.DecryptedMessageService17({ randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), action: new index_js_1.Raw.DecryptedMessageActionAcceptKey20({ gB: await (0, helpers_js_1.bigintToBuffer)(gB, 256, false), exchangeId: action.exchangeId, keyFingerprint: fingerprint.readBigInt64LE(), }), })), })); } async commitRekeying(chatId, action) { Logger_js_1.Logger.debug(`[120] re-keying ${chatId}: commiting`); const peer = await this._storage.getSecretChatById(chatId); if (!peer) { throw new index_js_3.SecretChatError.ChatNotFound(chatId); } if (peer.rekeyStep !== 1 || !this._tempAuthKey.has(action.exchangeId)) { const release = await this._mutex.acquire(); try { peer.rekeyStep = 0; peer.rekeyExchange = BigInt(0); await peer.update(this._storage); } finally { release(); } return; } const dh = await this.reqDHConfig(); const p = await (0, helpers_js_1.bufferToBigint)(dh.p, false); const gB = await (0, helpers_js_1.bufferToBigint)(action.gB, false); const authKey = await (0, helpers_js_1.bigintToBuffer)((0, helpers_js_1.bigIntPow)(gB, await (0, helpers_js_1.bufferToBigint)(this._tempAuthKey.get(action.exchangeId)), p), 256, false); const fingerprint = sha1(authKey).subarray(-8).readBigInt64LE(); index_js_3.SecurityCheckMismatch.check(BigInt(1) < gB && gB < p - BigInt(1), 'gB must be greater than one and smaller than p-1'); index_js_3.SecurityCheckMismatch.check(BigInt(2) ** BigInt(2048 - 64) < gB && gB < p - BigInt(2) ** BigInt(2048 - 64), 'gB must be greater than 2^{2048 - 64} and smaller than p-2^{2048 -64}'); Logger_js_1.Logger.debug('[121] gB validation: OK'); if (fingerprint !== action.keyFingerprint) { Logger_js_1.Logger.error(`[122] re-keying ${chatId}: Aborting due mismatched fingerprint`); await this._client.invoke(new index_js_1.Raw.messages.SendEncryptedService({ peer: peer.input, randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), data: await this.encrypt(chatId, new index_js_1.Raw.DecryptedMessageService17({ randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), action: new index_js_1.Raw.DecryptedMessageActionAbortKey20({ exchangeId: action.exchangeId, }), })), })); throw new index_js_3.SecretChatError.FingerprintMismatch(); } const response = await this._client.invoke(new index_js_1.Raw.messages.SendEncryptedService({ peer: peer.input, randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), data: await this.encrypt(chatId, new index_js_1.Raw.DecryptedMessageService17({ randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), action: new index_js_1.Raw.DecryptedMessageActionCommitKey20({ exchangeId: action.exchangeId, keyFingerprint: action.keyFingerprint, }), })), })); const release = await this._mutex.acquire(); try { this._tempAuthKey.delete(action.exchangeId); peer.rekeyStep = 0; peer.rekeyExchange = BigInt(0); peer.authKey = authKey; peer.timeRekey = 100; peer.changed = Date.now() / 1000; await peer.update(this._storage); } finally { release(); } return response; } async finalRekeying(chatId, action) { Logger_js_1.Logger.debug(`[123] re-keying ${chatId}: finishing`); const peer = await this._storage.getSecretChatById(chatId); if (!peer) { throw new index_js_3.SecretChatError.ChatNotFound(chatId); } if (peer.rekeyStep !== 2 || !this._tempAuthKey.has(action.exchangeId)) { return; } const fingerprint = sha1(this._tempAuthKey.get(action.exchangeId)) .subarray(-8) .readBigInt64LE(); if (fingerprint !== action.keyFingerprint) { Logger_js_1.Logger.error(`[124] re-keying ${chatId}: Aborting due mismatched fingerprint`); await this._client.invoke(new index_js_1.Raw.messages.SendEncryptedService({ peer: peer.input, randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), data: await this.encrypt(chatId, new index_js_1.Raw.DecryptedMessageService17({ randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), action: new index_js_1.Raw.DecryptedMessageActionAbortKey20({ exchangeId: action.exchangeId, }), })), })); throw new index_js_3.SecretChatError.FingerprintMismatch(); } const release = await this._mutex.acquire(); try { peer.rekeyStep = 0; peer.rekeyExchange = BigInt(0); peer.authKey = this._tempAuthKey.get(action.exchangeId); peer.timeRekey = 100; peer.changed = Date.now() / 1000; this._tempAuthKey.delete(action.exchangeId); await peer.update(this._storage); } finally { release(); } return this._client.invoke(new index_js_1.Raw.messages.SendEncryptedService({ peer: peer.input, randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), data: await this.encrypt(chatId, new index_js_1.Raw.DecryptedMessageService17({ randomId: platform_node_js_1.Buffer.from(platform_node_js_1.crypto.randomBytes(8)).readBigInt64LE(), action: new index_js_1.Raw.DecryptedMessageActionNoop20(), })), })); } async decrypt(message) { let decrypted; if (!this._waiting.includes(message.chatId)) { const peer = await this._storage.getSecretChatById(message.chatId); if (!peer) { throw new index_js_3.SecretChatError.ChatNotFound(message.chatId); } if (peer.mtproto === 2) { try { decrypted = await index_js_4.SecretChats.unpack(message, peer.authKey, peer.isAdmin, peer.mtproto); } catch (error) { if (error instanceof index_js_3.SecretChatError.FingerprintMismatch) { await this.destroy(message.chatId); throw error; } decrypted = await index_js_4.SecretChats.unpack(message, peer.authKey, peer.isAdmin, 1); peer.mtproto = 1; Logger_js_1.Logger.debug(`[112] Switch MTProto version for ${message.chatId} to ${peer.mtproto}`); } } else { try { decrypted = await index_js_4.SecretChats.unpack(message, peer.authKey, peer.isAdmin, peer.mtproto); } catch (error) { if (error instanceof index_js_3.SecretChatError.FingerprintMismatch) { await this.destroy(message.chatId); throw error; } decrypted = await index_js_4.SecretChats.unpack(message, peer.authKey, peer.isAdmin, 2); peer.mtproto = 2; Logger_js_1.Logger.debug(`[113] Switch MTProto version for ${message.chatId} to ${peer.mtproto}`); } } const release = await this._mutex.acquire(); try { peer.timeRekey -= 1; await peer.update(this._storage); } finally { release(); } if ((peer.timeRekey <= 0 || Date.now() / 1000 - peer.changed < 7 * 24 * 60 * 60) && peer.rekeyStep === 0) { await this.rekeying(message.chatId); } } return decrypted; } async encrypt(chatId, message) { const peer = await this._storage.getSecretChatById(chatId); if (!peer) { throw new index_js_3.SecretChatError.ChatNotFound(chatId); } const release = await this._mutex.acquire(); const inSeqNo = peer.inSeqNo * 2 + peer.inSeqNoX; const outSeqNo = peer.outSeqNo * 2 + peer.outSeqNoX; try { peer.timeRekey -= 1; peer.inSeqNo = inSeqNo; peer.outSeqNo = outSeqNo; await peer.update(this._storage); } finally { release(); } if (peer.layer > 8) { if ((peer.timeRekey <= 0 || Date.now() / 1000 - peer.changed < 7 * 24 * 60 * 60) && peer.rekeyStep === 0) { await this.rekeying(chatId); } } return index_js_4.SecretChats.pack(message, peer.authKey, inSeqNo, outSeqNo, peer.isAdmin, peer.layer, peer.mtproto); } [Symbol.for('nodejs.util.inspect.custom')]() { const toPrint = { _: this.constructor.name, }; for (const key in this) { if (Object.prototype.hasOwnProperty.call(this, key)) { const value = this[key]; if (!key.startsWith('_') && value !== undefined && value !== null) { toPrint[key] = value; } } } return toPrint; } [Symbol.for('Deno.customInspect')]() { return String((0, platform_node_js_1.inspect)(this[Symbol.for('nodejs.util.inspect.custom')](), { colors: true })); } toJSON() { const toPrint = { _: this.constructor.name, }; for (const key in this) { if (Object.prototype.hasOwnProperty.call(this, key)) { const value = this[key]; if (!key.startsWith('_') && value !== undefined && value !== null) { if (typeof value === 'bigint') { toPrint[key] = String(value); } else if (Array.isArray(value)) { toPrint[key] = value.map((v) => (typeof v === 'bigint' ? String(v) : v)); } else { toPrint[key] = value; } } } } return toPrint; } toString() { return `[constructor of ${this.constructor.name}] ${JSON.stringify(this, null, 2)}`; } } exports.SecretChat = SecretChat;