@tgsnake/core
Version:
Pure Telegram MTProto library for nodejs
528 lines (527 loc) • 25.5 kB
JavaScript
"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;