@mtkruto/node 0.161.0 → 0.171.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/0_deps.d.ts +1 -1
- package/esm/0_deps.d.ts.map +1 -1
- package/esm/0_deps.js +1 -1
- package/esm/2_tl.d.ts +1 -0
- package/esm/2_tl.d.ts.map +1 -1
- package/esm/2_tl.js +1 -0
- package/esm/3_types.d.ts +3 -0
- package/esm/3_types.d.ts.map +1 -1
- package/esm/3_types.js +3 -0
- package/esm/client/0_markdown.d.ts.map +1 -1
- package/esm/client/0_params.d.ts +39 -5
- package/esm/client/0_params.d.ts.map +1 -1
- package/esm/client/0_secret_chat_state.d.ts +86 -0
- package/esm/client/0_secret_chat_state.d.ts.map +1 -0
- package/esm/client/0_secret_chat_state.js +129 -0
- package/esm/client/0_storage_operations.d.ts +1 -0
- package/esm/client/0_storage_operations.d.ts.map +1 -1
- package/esm/client/0_storage_operations.js +3 -0
- package/esm/client/0_utilities.d.ts +2 -2
- package/esm/client/0_utilities.js +2 -2
- package/esm/client/1_client_generic.d.ts +87 -16
- package/esm/client/1_client_generic.d.ts.map +1 -1
- package/esm/client/2_client_encrypted.js +6 -6
- package/esm/client/2_file_manager.d.ts.map +1 -1
- package/esm/client/2_file_manager.js +26 -3
- package/esm/client/3_account_manager.js +3 -3
- package/esm/client/3_filters.d.ts +22 -7
- package/esm/client/3_filters.d.ts.map +1 -1
- package/esm/client/3_message_manager.d.ts +59 -20
- package/esm/client/3_message_manager.d.ts.map +1 -1
- package/esm/client/3_message_manager.js +6 -0
- package/esm/client/3_secret_chat_manager.d.ts +26 -0
- package/esm/client/3_secret_chat_manager.d.ts.map +1 -0
- package/esm/client/3_secret_chat_manager.js +770 -0
- package/esm/client/4_chat_manager.d.ts +2 -0
- package/esm/client/4_chat_manager.d.ts.map +1 -1
- package/esm/client/4_chat_manager.js +12 -0
- package/esm/client/4_context.d.ts +18 -3
- package/esm/client/4_context.d.ts.map +1 -1
- package/esm/client/4_context.js +65 -10
- package/esm/client/4_poll_manager.js +2 -2
- package/esm/client/6_client.d.ts +88 -17
- package/esm/client/6_client.d.ts.map +1 -1
- package/esm/client/6_client.js +119 -19
- package/esm/client/6_client_dispatcher.d.ts +87 -16
- package/esm/client/6_client_dispatcher.d.ts.map +1 -1
- package/esm/client/6_client_dispatcher.js +108 -16
- package/esm/session/2_session_encrypted.js +5 -5
- package/esm/tl/1_secret_chats_api.d.ts +637 -0
- package/esm/tl/1_secret_chats_api.d.ts.map +1 -0
- package/esm/tl/1_secret_chats_api.js +849 -0
- package/esm/tl/1_tl_reader.js +3 -3
- package/esm/tl/2_secret_chats.d.ts +33 -0
- package/esm/tl/2_secret_chats.d.ts.map +1 -0
- package/esm/tl/2_secret_chats.js +53 -0
- package/esm/types/0_secret_chat.d.ts +66 -0
- package/esm/types/0_secret_chat.d.ts.map +1 -0
- package/esm/types/0_secret_chat.js +30 -0
- package/esm/types/0_secret_message_entity.d.ts +115 -0
- package/esm/types/0_secret_message_entity.d.ts.map +1 -0
- package/esm/types/0_secret_message_entity.js +174 -0
- package/esm/types/1_chat_p.d.ts +3 -3
- package/esm/types/1_chat_p.d.ts.map +1 -1
- package/esm/types/1_input_poll_media.d.ts +2 -2
- package/esm/types/1_photo.d.ts +3 -2
- package/esm/types/1_photo.d.ts.map +1 -1
- package/esm/types/1_sticker.d.ts +5 -4
- package/esm/types/1_sticker.d.ts.map +1 -1
- package/esm/types/1_sticker.js +23 -1
- package/esm/types/1_video.d.ts +2 -2
- package/esm/types/1_video.d.ts.map +1 -1
- package/esm/types/1_video_note.d.ts +1 -1
- package/esm/types/1_video_note.d.ts.map +1 -1
- package/esm/types/2_inactive_chat.d.ts +1 -1
- package/esm/types/2_inactive_chat.d.ts.map +1 -1
- package/esm/types/2_left_channel_list.d.ts +1 -1
- package/esm/types/2_left_channel_list.d.ts.map +1 -1
- package/esm/types/2_message_entity.d.ts +0 -3
- package/esm/types/2_message_entity.d.ts.map +1 -1
- package/esm/types/2_secret_message.d.ts +184 -0
- package/esm/types/2_secret_message.d.ts.map +1 -0
- package/esm/types/2_secret_message.js +341 -0
- package/esm/types/2_user.d.ts +2 -2
- package/esm/types/2_user.d.ts.map +1 -1
- package/esm/types/3_input_media.d.ts +2 -2
- package/esm/types/4_gift.d.ts +2 -2
- package/esm/types/4_gift.d.ts.map +1 -1
- package/esm/types/9_message.d.ts +9 -61
- package/esm/types/9_message.d.ts.map +1 -1
- package/esm/types/9_message.js +1 -0
- package/esm/types/B_update.d.ts +33 -42
- package/esm/types/B_update.d.ts.map +1 -1
- package/package.json +1 -1
- package/script/0_deps.d.ts +1 -1
- package/script/0_deps.d.ts.map +1 -1
- package/script/0_deps.js +2 -1
- package/script/2_tl.d.ts +1 -0
- package/script/2_tl.d.ts.map +1 -1
- package/script/2_tl.js +2 -1
- package/script/3_types.d.ts +3 -0
- package/script/3_types.d.ts.map +1 -1
- package/script/3_types.js +3 -0
- package/script/client/0_markdown.d.ts.map +1 -1
- package/script/client/0_params.d.ts +39 -5
- package/script/client/0_params.d.ts.map +1 -1
- package/script/client/0_secret_chat_state.d.ts +86 -0
- package/script/client/0_secret_chat_state.d.ts.map +1 -0
- package/script/client/0_secret_chat_state.js +133 -0
- package/script/client/0_storage_operations.d.ts +1 -0
- package/script/client/0_storage_operations.d.ts.map +1 -1
- package/script/client/0_storage_operations.js +3 -0
- package/script/client/0_utilities.d.ts +2 -2
- package/script/client/0_utilities.js +2 -2
- package/script/client/1_client_generic.d.ts +87 -16
- package/script/client/1_client_generic.d.ts.map +1 -1
- package/script/client/2_client_encrypted.js +6 -6
- package/script/client/2_file_manager.d.ts.map +1 -1
- package/script/client/2_file_manager.js +25 -2
- package/script/client/3_account_manager.js +3 -3
- package/script/client/3_filters.d.ts +22 -7
- package/script/client/3_filters.d.ts.map +1 -1
- package/script/client/3_message_manager.d.ts +59 -20
- package/script/client/3_message_manager.d.ts.map +1 -1
- package/script/client/3_message_manager.js +6 -0
- package/script/client/3_secret_chat_manager.d.ts +26 -0
- package/script/client/3_secret_chat_manager.d.ts.map +1 -0
- package/script/client/3_secret_chat_manager.js +807 -0
- package/script/client/4_chat_manager.d.ts +2 -0
- package/script/client/4_chat_manager.d.ts.map +1 -1
- package/script/client/4_chat_manager.js +12 -0
- package/script/client/4_context.d.ts +18 -3
- package/script/client/4_context.d.ts.map +1 -1
- package/script/client/4_context.js +65 -10
- package/script/client/4_poll_manager.js +2 -2
- package/script/client/6_client.d.ts +88 -17
- package/script/client/6_client.d.ts.map +1 -1
- package/script/client/6_client.js +119 -19
- package/script/client/6_client_dispatcher.d.ts +87 -16
- package/script/client/6_client_dispatcher.d.ts.map +1 -1
- package/script/client/6_client_dispatcher.js +108 -16
- package/script/session/2_session_encrypted.js +5 -5
- package/script/tl/1_secret_chats_api.d.ts +637 -0
- package/script/tl/1_secret_chats_api.d.ts.map +1 -0
- package/script/tl/1_secret_chats_api.js +852 -0
- package/script/tl/1_tl_reader.js +3 -3
- package/script/tl/2_secret_chats.d.ts +33 -0
- package/script/tl/2_secret_chats.d.ts.map +1 -0
- package/script/tl/2_secret_chats.js +78 -0
- package/script/types/0_secret_chat.d.ts +66 -0
- package/script/types/0_secret_chat.d.ts.map +1 -0
- package/script/types/0_secret_chat.js +33 -0
- package/script/types/0_secret_message_entity.d.ts +115 -0
- package/script/types/0_secret_message_entity.d.ts.map +1 -0
- package/script/types/0_secret_message_entity.js +179 -0
- package/script/types/1_chat_p.d.ts +3 -3
- package/script/types/1_chat_p.d.ts.map +1 -1
- package/script/types/1_input_poll_media.d.ts +2 -2
- package/script/types/1_photo.d.ts +3 -2
- package/script/types/1_photo.d.ts.map +1 -1
- package/script/types/1_sticker.d.ts +5 -4
- package/script/types/1_sticker.d.ts.map +1 -1
- package/script/types/1_sticker.js +23 -0
- package/script/types/1_video.d.ts +2 -2
- package/script/types/1_video.d.ts.map +1 -1
- package/script/types/1_video_note.d.ts +1 -1
- package/script/types/1_video_note.d.ts.map +1 -1
- package/script/types/2_inactive_chat.d.ts +1 -1
- package/script/types/2_inactive_chat.d.ts.map +1 -1
- package/script/types/2_left_channel_list.d.ts +1 -1
- package/script/types/2_left_channel_list.d.ts.map +1 -1
- package/script/types/2_message_entity.d.ts +0 -3
- package/script/types/2_message_entity.d.ts.map +1 -1
- package/script/types/2_secret_message.d.ts +184 -0
- package/script/types/2_secret_message.d.ts.map +1 -0
- package/script/types/2_secret_message.js +344 -0
- package/script/types/2_user.d.ts +2 -2
- package/script/types/2_user.d.ts.map +1 -1
- package/script/types/3_input_media.d.ts +2 -2
- package/script/types/4_gift.d.ts +2 -2
- package/script/types/4_gift.d.ts.map +1 -1
- package/script/types/9_message.d.ts +9 -61
- package/script/types/9_message.d.ts.map +1 -1
- package/script/types/9_message.js +1 -0
- package/script/types/B_update.d.ts +33 -42
- package/script/types/B_update.d.ts.map +1 -1
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
/**
|
|
3
|
+
* MTKruto - Cross-runtime JavaScript library for building Telegram clients
|
|
4
|
+
* Copyright (C) 2023-2026 Roj <https://roj.im/>
|
|
5
|
+
*
|
|
6
|
+
* This file is part of MTKruto.
|
|
7
|
+
*
|
|
8
|
+
* This program is free software: you can redistribute it and/or modify
|
|
9
|
+
* it under the terms of the GNU Lesser General Public License as published by
|
|
10
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
* (at your option) any later version.
|
|
12
|
+
*
|
|
13
|
+
* This program is distributed in the hope that it will be useful,
|
|
14
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
* GNU Lesser General Public License for more details.
|
|
17
|
+
*
|
|
18
|
+
* You should have received a copy of the GNU Lesser General Public License
|
|
19
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
*/
|
|
21
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
22
|
+
import { concat, equals, ige256Decrypt, ige256Encrypt, WEEK } from "../0_deps.js";
|
|
23
|
+
import { InputError } from "../0_errors.js";
|
|
24
|
+
import { getLogger, getRandomId, getRandomInt, intFromBytes, intToBytes, mod, modExp, sha1, sha256 } from "../1_utilities.js";
|
|
25
|
+
import { Api, SecretChats, TLReader, TLWriter, X } from "../2_tl.js";
|
|
26
|
+
import { secretMessageEntityToTlObject } from "../3_types.js";
|
|
27
|
+
import { constructSecretChat } from "../types/0_secret_chat.js";
|
|
28
|
+
import { constructSecretMessage } from "../types/2_secret_message.js";
|
|
29
|
+
import { isGoodModExpFirst, isSafePrime } from "./0_password.js";
|
|
30
|
+
import { SecretChatState } from "./0_secret_chat_state.js";
|
|
31
|
+
const secretChatManagerUpdates = [
|
|
32
|
+
"updateEncryption",
|
|
33
|
+
"updateNewEncryptedMessage",
|
|
34
|
+
];
|
|
35
|
+
export class SecretChatManager {
|
|
36
|
+
#c;
|
|
37
|
+
#L;
|
|
38
|
+
constructor(c) {
|
|
39
|
+
this.#c = c;
|
|
40
|
+
this.#L = getLogger("SecretChatManager");
|
|
41
|
+
}
|
|
42
|
+
async loadSecretChats() {
|
|
43
|
+
let loaded = 0;
|
|
44
|
+
for await (const [k, v] of await this.#c.messageStorage.storage.getMany({ prefix: ["secretChats"] })) {
|
|
45
|
+
if (typeof k[1] !== "number") {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
this.#states.set(k[1], SecretChatState.load(v));
|
|
49
|
+
++loaded;
|
|
50
|
+
}
|
|
51
|
+
this.#L.debug("loaded", loaded, "secret chats");
|
|
52
|
+
}
|
|
53
|
+
static #checkDhConfig(dhConfig) {
|
|
54
|
+
const prime = intFromBytes(dhConfig.p, { byteOrder: "big", isSigned: false });
|
|
55
|
+
if (prime.toString(2).length !== 2048) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
let mod_ok = false;
|
|
59
|
+
let mod_r = 0;
|
|
60
|
+
switch (dhConfig.g) {
|
|
61
|
+
case 2:
|
|
62
|
+
mod_ok = prime % 8n === 7n;
|
|
63
|
+
break;
|
|
64
|
+
case 3:
|
|
65
|
+
mod_ok = prime % 3n === 2n;
|
|
66
|
+
break;
|
|
67
|
+
case 4:
|
|
68
|
+
mod_ok = true;
|
|
69
|
+
break;
|
|
70
|
+
case 5:
|
|
71
|
+
mod_ok = (mod_r = Number(prime % 5n)) === 1 || mod_r === 4;
|
|
72
|
+
break;
|
|
73
|
+
case 6:
|
|
74
|
+
mod_ok = (mod_r = Number(prime % 24n)) === 19 || mod_r === 23;
|
|
75
|
+
break;
|
|
76
|
+
case 7:
|
|
77
|
+
mod_ok = (mod_r = Number(prime % 7n)) === 3 || mod_r === 5 || mod_r === 6;
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
mod_ok = false;
|
|
81
|
+
}
|
|
82
|
+
return mod_ok && (isSafePrime(dhConfig.p, dhConfig.g));
|
|
83
|
+
}
|
|
84
|
+
async #getDhConfig() {
|
|
85
|
+
const result = Api.as("messages.dhConfig", await this.#c.invoke({
|
|
86
|
+
_: "messages.getDhConfig",
|
|
87
|
+
version: 0,
|
|
88
|
+
random_length: 256,
|
|
89
|
+
}));
|
|
90
|
+
if (!_a.#checkDhConfig(result)) {
|
|
91
|
+
throw new TypeError("Received invalid dhConfig.");
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
#states = new Map();
|
|
96
|
+
#getSecretChatState(id) {
|
|
97
|
+
let state = this.#states.get(id);
|
|
98
|
+
if (state === undefined) {
|
|
99
|
+
state = new SecretChatState();
|
|
100
|
+
this.#states.set(id, state);
|
|
101
|
+
}
|
|
102
|
+
return state;
|
|
103
|
+
}
|
|
104
|
+
async requestSecretChat(chatId) {
|
|
105
|
+
const user_id = await this.#c.getInputUser(chatId);
|
|
106
|
+
if (Api.is("inputUserSelf", user_id)) {
|
|
107
|
+
throw new InputError("Received invalid chat identifier.");
|
|
108
|
+
}
|
|
109
|
+
const dhConfig = await this.#getDhConfig();
|
|
110
|
+
const prime = intFromBytes(dhConfig.p, { byteOrder: "big", isSigned: false });
|
|
111
|
+
const a = getRandomInt(256, false);
|
|
112
|
+
const g_a = intToBytes(modExp(BigInt(dhConfig.g), a, prime), 256, { byteOrder: "big", isSigned: false });
|
|
113
|
+
const result = await this.#c.invoke({
|
|
114
|
+
_: "messages.requestEncryption",
|
|
115
|
+
user_id,
|
|
116
|
+
g_a,
|
|
117
|
+
random_id: getRandomId(true),
|
|
118
|
+
});
|
|
119
|
+
const state = this.#getSecretChatState(result.id);
|
|
120
|
+
state.g = dhConfig.g;
|
|
121
|
+
state.prime = prime;
|
|
122
|
+
state.a = a;
|
|
123
|
+
state.pendingExponent = a;
|
|
124
|
+
return constructSecretChat(result);
|
|
125
|
+
}
|
|
126
|
+
async acceptSecretChat(id) {
|
|
127
|
+
const state = this.#getSecretChatState(id);
|
|
128
|
+
if (!Api.is("encryptedChatRequested", state.encryptedChat)) {
|
|
129
|
+
throw new InputError("Invalid secret chat identifier received.");
|
|
130
|
+
}
|
|
131
|
+
const dhConfig = await this.#getDhConfig();
|
|
132
|
+
const prime = intFromBytes(dhConfig.p, { byteOrder: "big", isSigned: false });
|
|
133
|
+
state.g = dhConfig.g;
|
|
134
|
+
state.prime = prime;
|
|
135
|
+
const b = getRandomInt(256, false);
|
|
136
|
+
// key = (pow(g_a, b) mod dh_prime)
|
|
137
|
+
const gA = intFromBytes(state.encryptedChat.g_a, { byteOrder: "big", isSigned: false });
|
|
138
|
+
if (!isGoodModExpFirst(gA, prime)) {
|
|
139
|
+
throw new TypeError("Received invalid g_a.");
|
|
140
|
+
}
|
|
141
|
+
let authKey = intToBytes(modExp(gA, b, prime), 256, { byteOrder: "big", isSigned: false });
|
|
142
|
+
if (authKey.byteLength < 256) {
|
|
143
|
+
authKey = concat([new Uint8Array(256 - authKey.byteLength), authKey]);
|
|
144
|
+
}
|
|
145
|
+
state.authKey = authKey;
|
|
146
|
+
state.authKeyCreatedAt = Date.now();
|
|
147
|
+
state.authKeyUseCount = 0;
|
|
148
|
+
state.isAuthKeyUsed = false;
|
|
149
|
+
const authKeyId = (await sha1(authKey)).subarray(-8);
|
|
150
|
+
state.authKeyId_ = authKeyId;
|
|
151
|
+
const key_fingerprint = intFromBytes(authKeyId);
|
|
152
|
+
state.authKeyId = key_fingerprint;
|
|
153
|
+
// g_b := pow(g, b) mod dh_prime
|
|
154
|
+
const g_b = intToBytes(modExp(BigInt(dhConfig.g), b, prime), 256, { byteOrder: "big", isSigned: false });
|
|
155
|
+
const peer = { _: "inputEncryptedChat", chat_id: state.encryptedChat.id, access_hash: state.encryptedChat.access_hash };
|
|
156
|
+
const result = await this.#c.invoke({
|
|
157
|
+
_: "messages.acceptEncryption",
|
|
158
|
+
peer,
|
|
159
|
+
g_b,
|
|
160
|
+
key_fingerprint,
|
|
161
|
+
});
|
|
162
|
+
state.encryptedChat = result;
|
|
163
|
+
return constructSecretChat(result);
|
|
164
|
+
}
|
|
165
|
+
#getNextOutSeqNo(id, isCreator) {
|
|
166
|
+
const state = this.#getSecretChatState(id);
|
|
167
|
+
const rawOutSeqNo = state.outSeqNo;
|
|
168
|
+
state.outSeqNo = rawOutSeqNo + 1;
|
|
169
|
+
return 2 * rawOutSeqNo + (isCreator ? 1 : 0);
|
|
170
|
+
}
|
|
171
|
+
#getInSeqNo(id, isCreator) {
|
|
172
|
+
const state = this.#getSecretChatState(id);
|
|
173
|
+
const rawInSeqNo = state.inSeqNo;
|
|
174
|
+
return 2 * rawInSeqNo + (isCreator ? 0 : 1);
|
|
175
|
+
}
|
|
176
|
+
#mustGetEncryptedChat(id) {
|
|
177
|
+
const state = this.#getSecretChatState(id);
|
|
178
|
+
if (!Api.is("encryptedChat", state.encryptedChat)) {
|
|
179
|
+
throw new InputError("Received invalid secret chat identifier.");
|
|
180
|
+
}
|
|
181
|
+
return state;
|
|
182
|
+
}
|
|
183
|
+
async #postSendMessage(state) {
|
|
184
|
+
state.isJustLoaded = false;
|
|
185
|
+
await this.#maybeStartRekey(state);
|
|
186
|
+
}
|
|
187
|
+
async sendSecretMessage(id, text, params) {
|
|
188
|
+
this.#c.storage.assertUser("sendSecretMessage");
|
|
189
|
+
const state = this.#mustGetEncryptedChat(id);
|
|
190
|
+
const random_id = getRandomId();
|
|
191
|
+
const decryptedMessage = {
|
|
192
|
+
_: "decryptedMessage",
|
|
193
|
+
message: text,
|
|
194
|
+
random_id,
|
|
195
|
+
ttl: params?.ttl ?? 0,
|
|
196
|
+
silent: params?.isSilent || undefined,
|
|
197
|
+
reply_to_random_id: params?.replyToMessageId ? BigInt(params.replyToMessageId) : undefined,
|
|
198
|
+
entities: params?.entities?.length ? params.entities.map(secretMessageEntityToTlObject) : undefined,
|
|
199
|
+
via_bot_name: params?.viaBot,
|
|
200
|
+
};
|
|
201
|
+
await this.#sendMessage(decryptedMessage, state.encryptedChat, state.authKey, state.authKeyId_);
|
|
202
|
+
await this.#postSendMessage(state);
|
|
203
|
+
}
|
|
204
|
+
async sendSecretLocation(id, latitude, longitude, params) {
|
|
205
|
+
this.#c.storage.assertUser("sendSecretLocation");
|
|
206
|
+
const state = this.#mustGetEncryptedChat(id);
|
|
207
|
+
const random_id = getRandomId();
|
|
208
|
+
const decryptedMessage = {
|
|
209
|
+
_: "decryptedMessage",
|
|
210
|
+
message: "",
|
|
211
|
+
random_id,
|
|
212
|
+
ttl: params?.ttl ?? 0,
|
|
213
|
+
silent: params?.isSilent || undefined,
|
|
214
|
+
reply_to_random_id: params?.replyToMessageId ? BigInt(params.replyToMessageId) : undefined,
|
|
215
|
+
via_bot_name: params?.viaBot,
|
|
216
|
+
media: { _: "decryptedMessageMediaGeoPoint", lat: latitude, long: longitude },
|
|
217
|
+
};
|
|
218
|
+
await this.#sendMessage(decryptedMessage, state.encryptedChat, state.authKey, state.authKeyId_);
|
|
219
|
+
await this.#postSendMessage(state);
|
|
220
|
+
}
|
|
221
|
+
async sendSecretVenue(id, latitude, longitude, title, address, params) {
|
|
222
|
+
this.#c.storage.assertUser("sendSecretVenue");
|
|
223
|
+
const state = this.#mustGetEncryptedChat(id);
|
|
224
|
+
const random_id = getRandomId();
|
|
225
|
+
const decryptedMessage = {
|
|
226
|
+
_: "decryptedMessage",
|
|
227
|
+
message: "",
|
|
228
|
+
random_id,
|
|
229
|
+
ttl: params?.ttl ?? 0,
|
|
230
|
+
silent: params?.isSilent || undefined,
|
|
231
|
+
reply_to_random_id: params?.replyToMessageId ? BigInt(params.replyToMessageId) : undefined,
|
|
232
|
+
via_bot_name: params?.viaBot,
|
|
233
|
+
media: { _: "decryptedMessageMediaVenue", lat: latitude, long: longitude, title, address, provider: "foursquare", venue_id: params?.foursquareId ?? "" },
|
|
234
|
+
};
|
|
235
|
+
await this.#sendMessage(decryptedMessage, state.encryptedChat, state.authKey, state.authKeyId_);
|
|
236
|
+
await this.#postSendMessage(state);
|
|
237
|
+
}
|
|
238
|
+
async sendSecretContact(id, firstName, phoneNumber, params) {
|
|
239
|
+
this.#c.storage.assertUser("sendSecretContact");
|
|
240
|
+
const state = this.#mustGetEncryptedChat(id);
|
|
241
|
+
const random_id = getRandomId();
|
|
242
|
+
const decryptedMessage = {
|
|
243
|
+
_: "decryptedMessage",
|
|
244
|
+
message: "",
|
|
245
|
+
random_id,
|
|
246
|
+
ttl: params?.ttl ?? 0,
|
|
247
|
+
silent: params?.isSilent || undefined,
|
|
248
|
+
reply_to_random_id: params?.replyToMessageId ? BigInt(params.replyToMessageId) : undefined,
|
|
249
|
+
via_bot_name: params?.viaBot,
|
|
250
|
+
media: { _: "decryptedMessageMediaContact", first_name: firstName, last_name: params?.lastName ?? "", phone_number: phoneNumber, user_id: 0 },
|
|
251
|
+
};
|
|
252
|
+
await this.#sendMessage(decryptedMessage, state.encryptedChat, state.authKey, state.authKeyId_);
|
|
253
|
+
await this.#postSendMessage(state);
|
|
254
|
+
}
|
|
255
|
+
#sendTails = new Map();
|
|
256
|
+
async #sendMessage(message, encryptedChat, authKey, authKeyId) {
|
|
257
|
+
try {
|
|
258
|
+
await this.#sendMessageInner(message, encryptedChat, authKey, authKeyId);
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
await this.#getSecretChatState(encryptedChat.id).commit(this.#c.messageStorage.storage);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async #sendMessageInner(message, encryptedChat, authKey, authKeyId) {
|
|
265
|
+
const previous = this.#sendTails.get(encryptedChat.id) ?? Promise.resolve();
|
|
266
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
267
|
+
const tail = previous.then(() => promise);
|
|
268
|
+
this.#sendTails.set(encryptedChat.id, tail);
|
|
269
|
+
await previous;
|
|
270
|
+
try {
|
|
271
|
+
await this.#sendMessageUnlocked(message, encryptedChat, authKey, authKeyId);
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
resolve();
|
|
275
|
+
if (this.#sendTails.get(encryptedChat.id) === tail) {
|
|
276
|
+
this.#sendTails.delete(encryptedChat.id);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async #sendMessageUnlocked(message, encryptedChat, authKey, authKeyId) {
|
|
281
|
+
const random_id = getRandomId();
|
|
282
|
+
const isCreator = Number(encryptedChat.admin_id) === await this.#c.getSelfId();
|
|
283
|
+
const out_seq_no = this.#getNextOutSeqNo(encryptedChat.id, isCreator);
|
|
284
|
+
const in_seq_no = this.#getInSeqNo(encryptedChat.id, isCreator);
|
|
285
|
+
const decryptedMessageLayer = { _: "decryptedMessageLayer", in_seq_no, layer: 144, message, out_seq_no, random_bytes: dntShim.crypto.getRandomValues(new Uint8Array(15)) };
|
|
286
|
+
const data = await this.#encryptMessage(isCreator, authKeyId, authKey, decryptedMessageLayer);
|
|
287
|
+
this.#getSecretChatState(encryptedChat.id).outgoingMessages.set((out_seq_no - (isCreator ? 1 : 0)) / 2, data);
|
|
288
|
+
await this.#c.invoke({ _: "messages.sendEncrypted", peer: { _: "inputEncryptedChat", chat_id: encryptedChat.id, access_hash: encryptedChat.access_hash }, random_id, data });
|
|
289
|
+
const state = this.#getSecretChatState(encryptedChat.id);
|
|
290
|
+
if (equals(state.authKeyId_, authKeyId)) {
|
|
291
|
+
++state.authKeyUseCount;
|
|
292
|
+
state.isAuthKeyUsed = true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async #encryptMessage(isCreator, authKeyId, authKey, message) {
|
|
296
|
+
const serializedDecryptedMessageLayer = SecretChats.serializeObject(message);
|
|
297
|
+
const plainTextWriter = new TLWriter();
|
|
298
|
+
plainTextWriter.writeInt32(serializedDecryptedMessageLayer.byteLength, false);
|
|
299
|
+
plainTextWriter.write(serializedDecryptedMessageLayer);
|
|
300
|
+
let paddingLength = mod(-(4 + serializedDecryptedMessageLayer.byteLength), 16);
|
|
301
|
+
if (paddingLength < 12) {
|
|
302
|
+
paddingLength += 16;
|
|
303
|
+
}
|
|
304
|
+
plainTextWriter.write(dntShim.crypto.getRandomValues(new Uint8Array(paddingLength)));
|
|
305
|
+
const plainText = plainTextWriter.buffer;
|
|
306
|
+
const x = isCreator ? 0 : 8;
|
|
307
|
+
// msg_key_large = SHA256 (substr (key, 88+x, 32) + plaintext + random_padding);
|
|
308
|
+
const messageKeyLarge = await sha256(concat([authKey.subarray(88 + x, 88 + x + 32), plainText]));
|
|
309
|
+
// msg_key = substr (msg_key_large, 8, 16);
|
|
310
|
+
const messageKey = messageKeyLarge.subarray(8, 8 + 16);
|
|
311
|
+
// sha256_a = SHA256 (msg_key + substr (key, x, 36));
|
|
312
|
+
const sha256A = await sha256(concat([messageKey, authKey.subarray(x, x + 36)]));
|
|
313
|
+
// sha256_b = SHA256 (substr (key, 40+x, 36) + msg_key);
|
|
314
|
+
const sha256B = await sha256(concat([authKey.subarray(40 + x, 40 + x + 36), messageKey]));
|
|
315
|
+
// aes_key = substr (sha256_a, 0, 8) + substr (sha256_b, 8, 16) + substr (sha256_a, 24, 8);
|
|
316
|
+
const aesKey = concat([sha256A.subarray(0, 8), sha256B.subarray(8, 8 + 16), sha256A.subarray(24, 24 + 8)]);
|
|
317
|
+
// aes_iv = substr (sha256_b, 0, 8) + substr (sha256_a, 8, 16) + substr (sha256_b, 24, 8);
|
|
318
|
+
const aesIv = concat([sha256B.subarray(0, 8), sha256A.subarray(8, 8 + 16), sha256B.subarray(24, 24 + 8)]);
|
|
319
|
+
const encryptedText = ige256Encrypt(plainText, aesKey, aesIv);
|
|
320
|
+
const dataWriter = new TLWriter();
|
|
321
|
+
dataWriter.write(authKeyId);
|
|
322
|
+
dataWriter.write(messageKey);
|
|
323
|
+
dataWriter.write(encryptedText);
|
|
324
|
+
const data = dataWriter.buffer;
|
|
325
|
+
return data;
|
|
326
|
+
}
|
|
327
|
+
async #decryptMessage(authKeyId, authKey, isCreator, message) {
|
|
328
|
+
const x = isCreator ? 8 : 0;
|
|
329
|
+
if (message.byteLength < 40 || (message.byteLength - 24) % 16 !== 0) {
|
|
330
|
+
throw new TypeError("Received invalid encrypted message length.");
|
|
331
|
+
}
|
|
332
|
+
const messageReader = new TLReader(message);
|
|
333
|
+
if (!equals(messageReader.read(8), authKeyId)) {
|
|
334
|
+
throw new TypeError("Received invalid auth key identifier.");
|
|
335
|
+
}
|
|
336
|
+
const messageKey = messageReader.read(16);
|
|
337
|
+
// sha256_a = SHA256 (msg_key + substr (key, x, 36));
|
|
338
|
+
const sha256A = await sha256(concat([messageKey, authKey.subarray(x, x + 36)]));
|
|
339
|
+
// sha256_b = SHA256 (substr (key, 40+x, 36) + msg_key);
|
|
340
|
+
const sha256B = await sha256(concat([authKey.subarray(40 + x, 40 + x + 36), messageKey]));
|
|
341
|
+
// aes_key = substr (sha256_a, 0, 8) + substr (sha256_b, 8, 16) + substr (sha256_a, 24, 8);
|
|
342
|
+
const aesKey = concat([sha256A.subarray(0, 8), sha256B.subarray(8, 8 + 16), sha256A.subarray(24, 24 + 8)]);
|
|
343
|
+
// aes_iv = substr (sha256_b, 0, 8) + substr (sha256_a, 8, 16) + substr (sha256_b, 24, 8);
|
|
344
|
+
const aesIv = concat([sha256B.subarray(0, 8), sha256A.subarray(8, 8 + 16), sha256B.subarray(24, 24 + 8)]);
|
|
345
|
+
const decryptedText = ige256Decrypt(messageReader.buffer, aesKey, aesIv);
|
|
346
|
+
const expectedMessageKey = (await sha256(concat([authKey.subarray(88 + x, 88 + x + 32), decryptedText]))).subarray(8, 24);
|
|
347
|
+
if (!equals(messageKey, expectedMessageKey)) {
|
|
348
|
+
throw new TypeError("Encrypted message key mismatch.");
|
|
349
|
+
}
|
|
350
|
+
const decryptedTextReader = new TLReader(decryptedText);
|
|
351
|
+
const length = decryptedTextReader.readInt32(false);
|
|
352
|
+
const paddingLength = decryptedText.byteLength - 4 - length;
|
|
353
|
+
if (length < 0 || paddingLength < 12 || paddingLength > 1024) {
|
|
354
|
+
throw new TypeError("Received invalid encrypted message padding.");
|
|
355
|
+
}
|
|
356
|
+
const serializedMessage = decryptedTextReader.read(length);
|
|
357
|
+
return await SecretChats.deserializeType(X, serializedMessage);
|
|
358
|
+
}
|
|
359
|
+
async #checkGap(chatId, message, encryptedMessage) {
|
|
360
|
+
const state = this.#getSecretChatState(chatId);
|
|
361
|
+
if (!Api.is("encryptedChat", state.encryptedChat)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const isCreator = Number(state.encryptedChat.admin_id) === await this.#c.getSelfId();
|
|
365
|
+
const x = isCreator ? 0 : 1;
|
|
366
|
+
const inX = isCreator ? 1 : 0;
|
|
367
|
+
if (message.out_seq_no < 0 || message.out_seq_no % 2 !== x) {
|
|
368
|
+
this.#L.debug("discarding secret chat", chatId, "because an invalid out_seq_no was received");
|
|
369
|
+
await this.#c.invoke({ _: "messages.discardEncryption", chat_id: chatId });
|
|
370
|
+
throw new TypeError("Received invalid secret chat out_seq_no.");
|
|
371
|
+
}
|
|
372
|
+
const outSeqNo = (message.out_seq_no - x) / 2;
|
|
373
|
+
const inSeqNo = (message.in_seq_no - inX) / 2;
|
|
374
|
+
if (inSeqNo % 1 !== 0 || inSeqNo < 0 || inSeqNo > state.outSeqNo) {
|
|
375
|
+
this.#L.debug("discarding secret chat", chatId, "because an invalid in_seq_no was received");
|
|
376
|
+
await this.#c.invoke({ _: "messages.discardEncryption", chat_id: chatId });
|
|
377
|
+
throw new TypeError("Received invalid secret chat in_seq_no.");
|
|
378
|
+
}
|
|
379
|
+
if (outSeqNo < state.inSeqNo) { // old
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const alreadyPending = state.pendingMessages.some((v) => v[0].out_seq_no === message.out_seq_no);
|
|
383
|
+
if (!alreadyPending && SecretChats.is("decryptedMessageService", message.message) && SecretChats.is("decryptedMessageActionResend", message.message.action)) {
|
|
384
|
+
await this.#resendMessages(state, message.message.action, isCreator);
|
|
385
|
+
}
|
|
386
|
+
if (outSeqNo > state.inSeqNo) { // gap
|
|
387
|
+
if (!state.pendingMessages.some((v) => v[0].out_seq_no === message.out_seq_no)) {
|
|
388
|
+
state.pendingMessages.push([message, encryptedMessage]);
|
|
389
|
+
}
|
|
390
|
+
if (state.isGapRequested) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
await this.#sendMessage({
|
|
394
|
+
_: "decryptedMessageService",
|
|
395
|
+
random_id: getRandomId(),
|
|
396
|
+
action: {
|
|
397
|
+
_: "decryptedMessageActionResend",
|
|
398
|
+
start_seq_no: state.inSeqNo * 2 + x,
|
|
399
|
+
end_seq_no: message.out_seq_no - 2,
|
|
400
|
+
},
|
|
401
|
+
}, state.encryptedChat, state.authKey, state.authKeyId_);
|
|
402
|
+
state.isGapRequested = true;
|
|
403
|
+
state.gapEndSeqNo = outSeqNo - 1;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const handle = async (message, encryptedMessage) => {
|
|
407
|
+
const inSeqNo = (message.in_seq_no - inX) / 2;
|
|
408
|
+
if (inSeqNo < state.remoteInSeqNo && !state.isJustLoaded) {
|
|
409
|
+
this.#L.debug("discarding secret chat", chatId, "because of decreasing in_seq_no");
|
|
410
|
+
await this.#c.invoke({ _: "messages.discardEncryption", chat_id: chatId });
|
|
411
|
+
throw new TypeError("Received decreasing secret chat in_seq_no.");
|
|
412
|
+
}
|
|
413
|
+
state.remoteInSeqNo = Math.max(state.remoteInSeqNo, inSeqNo);
|
|
414
|
+
++state.inSeqNo;
|
|
415
|
+
await this.#handleDecryptedMessageLayer(chatId, message, encryptedMessage);
|
|
416
|
+
};
|
|
417
|
+
await handle(message, encryptedMessage);
|
|
418
|
+
while (true) {
|
|
419
|
+
const index = state.pendingMessages.findIndex((v) => (v[0].out_seq_no - x) / 2 === state.inSeqNo);
|
|
420
|
+
if (index === -1) {
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
const [pendingMessage] = state.pendingMessages.splice(index, 1);
|
|
424
|
+
await handle(pendingMessage[0], pendingMessage[1]);
|
|
425
|
+
}
|
|
426
|
+
if (state.pendingMessages.length === 0) {
|
|
427
|
+
state.isGapRequested = false;
|
|
428
|
+
state.gapEndSeqNo = -1;
|
|
429
|
+
}
|
|
430
|
+
else if (state.inSeqNo > state.gapEndSeqNo) {
|
|
431
|
+
state.isGapRequested = false;
|
|
432
|
+
const next = state.pendingMessages.reduce((a, b) => a[0].out_seq_no < b[0].out_seq_no ? a : b);
|
|
433
|
+
await this.#checkGap(chatId, next[0], next[1]);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async #resendMessages(state, action, isCreator) {
|
|
437
|
+
if (!Api.is("encryptedChat", state.encryptedChat)) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const x = isCreator ? 1 : 0;
|
|
441
|
+
const start = (action.start_seq_no - x) / 2;
|
|
442
|
+
const end = (action.end_seq_no - x) / 2;
|
|
443
|
+
if (start % 1 !== 0 || end % 1 !== 0 || start < 0 || end < start || end >= state.outSeqNo) {
|
|
444
|
+
this.#L.debug("discarding secret chat", state.encryptedChat.id, "because an invalid resend rage was received");
|
|
445
|
+
await this.#c.invoke({ _: "messages.discardEncryption", chat_id: state.encryptedChat.id });
|
|
446
|
+
throw new TypeError("Received invalid secret chat resend range.");
|
|
447
|
+
}
|
|
448
|
+
const peer = { _: "inputEncryptedChat", chat_id: state.encryptedChat.id, access_hash: state.encryptedChat.access_hash };
|
|
449
|
+
for (let seqNo = start; seqNo <= end; ++seqNo) {
|
|
450
|
+
const message = state.outgoingMessages.get(seqNo);
|
|
451
|
+
if (!message) {
|
|
452
|
+
this.#L.debug("discarding secret chat", state.encryptedChat.id, "because unable to resend message");
|
|
453
|
+
await this.#c.invoke({ _: "messages.discardEncryption", chat_id: state.encryptedChat.id });
|
|
454
|
+
throw new TypeError("Unable to resend secret chat message.");
|
|
455
|
+
}
|
|
456
|
+
await this.#c.invoke({ _: "messages.sendEncrypted", peer, random_id: getRandomId(), data: message });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
#clearPreviousKey(state) {
|
|
460
|
+
state.previousAuthKey.fill(0);
|
|
461
|
+
state.previousAuthKey = new Uint8Array();
|
|
462
|
+
state.previousAuthKeyId_ = new Uint8Array();
|
|
463
|
+
state.previousAuthKeyDiscardAfterSeqNo = -1;
|
|
464
|
+
state.isAwaitingNewAuthKeyConfirmation = false;
|
|
465
|
+
}
|
|
466
|
+
#installNewKey(state, authKey, authKeyId, authKeyId_, awaitingConfirmation, discardAfterSeqNo = -1) {
|
|
467
|
+
state.previousAuthKey = state.authKey;
|
|
468
|
+
state.previousAuthKeyId_ = state.authKeyId_;
|
|
469
|
+
state.previousAuthKeyDiscardAfterSeqNo = discardAfterSeqNo;
|
|
470
|
+
state.isAwaitingNewAuthKeyConfirmation = awaitingConfirmation;
|
|
471
|
+
state.authKey = authKey;
|
|
472
|
+
state.authKeyId = authKeyId;
|
|
473
|
+
state.authKeyId_ = authKeyId_;
|
|
474
|
+
state.authKeyCreatedAt = Date.now();
|
|
475
|
+
state.authKeyUseCount = 0;
|
|
476
|
+
state.isAuthKeyUsed = false;
|
|
477
|
+
}
|
|
478
|
+
async #abortRekey(state, exchangeId) {
|
|
479
|
+
if (!Api.is("encryptedChat", state.encryptedChat)) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const action = { _: "decryptedMessageActionAbortKey", exchange_id: exchangeId };
|
|
483
|
+
await this.#sendMessage({ _: "decryptedMessageService", random_id: getRandomId(), action }, state.encryptedChat, state.authKey, state.authKeyId_);
|
|
484
|
+
}
|
|
485
|
+
#clearInitiatedRekey(state) {
|
|
486
|
+
state.rekeyId = 0n;
|
|
487
|
+
state.rekeyA = 0n;
|
|
488
|
+
}
|
|
489
|
+
async #processDecryptedMessageActionAcceptKey(chatId, action_) {
|
|
490
|
+
const state = this.#getSecretChatState(chatId);
|
|
491
|
+
if (state.rekeyId !== action_.exchange_id || !Api.is("encryptedChat", state.encryptedChat)) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const gB = intFromBytes(action_.g_b, { byteOrder: "big", isSigned: false });
|
|
495
|
+
if (!isGoodModExpFirst(gB, state.prime)) {
|
|
496
|
+
await this.#abortRekey(state, action_.exchange_id);
|
|
497
|
+
this.#clearInitiatedRekey(state);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const authKey = intToBytes(modExp(gB, state.rekeyA, state.prime), 256, { byteOrder: "big", isSigned: false });
|
|
501
|
+
const authKeyId_ = (await sha1(authKey)).subarray(-8);
|
|
502
|
+
const authKeyId = intFromBytes(authKeyId_);
|
|
503
|
+
if (action_.key_fingerprint !== authKeyId) {
|
|
504
|
+
await this.#abortRekey(state, action_.exchange_id);
|
|
505
|
+
this.#clearInitiatedRekey(state);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const random_id = getRandomId();
|
|
509
|
+
const action = {
|
|
510
|
+
_: "decryptedMessageActionCommitKey",
|
|
511
|
+
exchange_id: state.rekeyId,
|
|
512
|
+
key_fingerprint: authKeyId,
|
|
513
|
+
};
|
|
514
|
+
await this.#sendMessage({ _: "decryptedMessageService", random_id, action }, state.encryptedChat, state.authKey, state.authKeyId_);
|
|
515
|
+
this.#installNewKey(state, authKey, authKeyId, authKeyId_, true);
|
|
516
|
+
this.#clearInitiatedRekey(state);
|
|
517
|
+
}
|
|
518
|
+
async #processDecryptedMessageActionRequestKey(chatId, action_) {
|
|
519
|
+
const state = this.#getSecretChatState(chatId);
|
|
520
|
+
if (!Api.is("encryptedChat", state.encryptedChat)) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (state.previousAuthKey.byteLength !== 0) {
|
|
524
|
+
await this.#abortRekey(state, action_.exchange_id);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (state.toCommitId !== 0n) {
|
|
528
|
+
await this.#abortRekey(state, action_.exchange_id);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (state.rekeyId !== 0n) {
|
|
532
|
+
if (state.rekeyId >= action_.exchange_id) {
|
|
533
|
+
if (state.rekeyId === action_.exchange_id) {
|
|
534
|
+
this.#clearInitiatedRekey(state);
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
this.#clearInitiatedRekey(state);
|
|
539
|
+
}
|
|
540
|
+
const gA = intFromBytes(action_.g_a, { byteOrder: "big", isSigned: false });
|
|
541
|
+
if (!isGoodModExpFirst(gA, state.prime)) {
|
|
542
|
+
await this.#abortRekey(state, action_.exchange_id);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
let b;
|
|
546
|
+
let gB;
|
|
547
|
+
do {
|
|
548
|
+
b = getRandomInt(256, false);
|
|
549
|
+
gB = modExp(BigInt(state.g), b, state.prime);
|
|
550
|
+
} while (!isGoodModExpFirst(gB, state.prime));
|
|
551
|
+
// pow(g_a, b) mod p
|
|
552
|
+
const authKey = intToBytes(modExp(gA, b, state.prime), 256, { byteOrder: "big", isSigned: false });
|
|
553
|
+
const authKeyId_ = (await sha1(authKey)).subarray(-8);
|
|
554
|
+
const authKeyId = intFromBytes(authKeyId_);
|
|
555
|
+
// pow(g,b) mod p
|
|
556
|
+
const g_b = intToBytes(gB, 256, { byteOrder: "big", isSigned: false });
|
|
557
|
+
const action = {
|
|
558
|
+
_: "decryptedMessageActionAcceptKey",
|
|
559
|
+
exchange_id: action_.exchange_id,
|
|
560
|
+
g_b,
|
|
561
|
+
key_fingerprint: authKeyId,
|
|
562
|
+
};
|
|
563
|
+
const random_id = getRandomId();
|
|
564
|
+
state.toCommitId = action_.exchange_id;
|
|
565
|
+
state.toCommitAuthKey = authKey;
|
|
566
|
+
state.toCommitAuthKeyId = authKeyId;
|
|
567
|
+
state.toCommitAuthKeyId_ = authKeyId_;
|
|
568
|
+
await this.#sendMessage({ _: "decryptedMessageService", random_id, action }, state.encryptedChat, state.authKey, state.authKeyId_);
|
|
569
|
+
}
|
|
570
|
+
async #processDecryptedMessageActionCommitKey(chatId, action) {
|
|
571
|
+
const state = this.#getSecretChatState(chatId);
|
|
572
|
+
if (state.toCommitId !== action.exchange_id) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (state.toCommitAuthKeyId !== action.key_fingerprint || !Api.is("encryptedChat", state.encryptedChat)) {
|
|
576
|
+
this.#L.debug(`discarding secret chat ${chatId}: re-key fingerprint mismatch`);
|
|
577
|
+
await this.#c.invoke({ _: "messages.discardEncryption", chat_id: chatId });
|
|
578
|
+
throw new TypeError("Secret chat re-key fingerprint mismatch.");
|
|
579
|
+
}
|
|
580
|
+
this.#installNewKey(state, state.toCommitAuthKey, state.toCommitAuthKeyId, state.toCommitAuthKeyId_, false);
|
|
581
|
+
state.toCommitId = 0n;
|
|
582
|
+
state.toCommitAuthKey = new Uint8Array();
|
|
583
|
+
state.toCommitAuthKeyId = 0n;
|
|
584
|
+
state.toCommitAuthKeyId_ = new Uint8Array();
|
|
585
|
+
const noop = { _: "decryptedMessageActionNoop" };
|
|
586
|
+
await this.#sendMessage({ _: "decryptedMessageService", random_id: getRandomId(), action: noop }, state.encryptedChat, state.authKey, state.authKeyId_);
|
|
587
|
+
this.#clearPreviousKey(state);
|
|
588
|
+
}
|
|
589
|
+
async #handleDecryptedMessageLayer(chatId, decryptedMessageLayer, encryptedMessage) {
|
|
590
|
+
const state = this.#getSecretChatState(chatId);
|
|
591
|
+
if (SecretChats.is("decryptedMessage", decryptedMessageLayer.message)) {
|
|
592
|
+
const secretMessage = constructSecretMessage(state.encryptedChat.id, decryptedMessageLayer.message, encryptedMessage);
|
|
593
|
+
this.#c.handleUpdate({ type: "secretMessage", secretMessage });
|
|
594
|
+
}
|
|
595
|
+
else if (SecretChats.is("decryptedMessageService", decryptedMessageLayer.message)) {
|
|
596
|
+
await this.#processServiceMessage(state.encryptedChat.id, decryptedMessageLayer.message);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async #processServiceMessage(chatId, message) {
|
|
600
|
+
switch (message.action._) {
|
|
601
|
+
case "decryptedMessageActionAcceptKey":
|
|
602
|
+
await this.#processDecryptedMessageActionAcceptKey(chatId, message.action);
|
|
603
|
+
break;
|
|
604
|
+
case "decryptedMessageActionRequestKey":
|
|
605
|
+
await this.#processDecryptedMessageActionRequestKey(chatId, message.action);
|
|
606
|
+
break;
|
|
607
|
+
case "decryptedMessageActionCommitKey":
|
|
608
|
+
await this.#processDecryptedMessageActionCommitKey(chatId, message.action);
|
|
609
|
+
break;
|
|
610
|
+
case "decryptedMessageActionAbortKey": {
|
|
611
|
+
const state = this.#getSecretChatState(chatId);
|
|
612
|
+
if (state.rekeyId === message.action.exchange_id) {
|
|
613
|
+
this.#clearInitiatedRekey(state);
|
|
614
|
+
}
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
async #processUpdateNewMessageEncrypted(update) {
|
|
620
|
+
try {
|
|
621
|
+
return await this.#processUpdateNewMessageEncryptedInner(update);
|
|
622
|
+
}
|
|
623
|
+
finally {
|
|
624
|
+
await this.#getSecretChatState(update.message.chat_id).commit(this.#c.messageStorage.storage);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
async #processUpdateNewMessageEncryptedInner(update) {
|
|
628
|
+
const state = this.#getSecretChatState(update.message.chat_id);
|
|
629
|
+
if (!Api.is("encryptedChat", state.encryptedChat)) {
|
|
630
|
+
this.#L.debug("ignoring encrypted message");
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
const isCreator = Number(state.encryptedChat.admin_id) === await this.#c.getSelfId();
|
|
634
|
+
const receivedKeyId = update.message.bytes.subarray(0, 8);
|
|
635
|
+
let authKey = state.authKey;
|
|
636
|
+
let authKeyId = state.authKeyId_;
|
|
637
|
+
let pendingKey = false;
|
|
638
|
+
if (equals(receivedKeyId, state.toCommitAuthKeyId_)) {
|
|
639
|
+
authKey = state.toCommitAuthKey;
|
|
640
|
+
authKeyId = state.toCommitAuthKeyId_;
|
|
641
|
+
pendingKey = true;
|
|
642
|
+
}
|
|
643
|
+
else if (equals(receivedKeyId, state.previousAuthKeyId_)) {
|
|
644
|
+
authKey = state.previousAuthKey;
|
|
645
|
+
authKeyId = state.previousAuthKeyId_;
|
|
646
|
+
}
|
|
647
|
+
const decryptedMessage = await this.#decryptMessage(authKeyId, authKey, isCreator, update.message.bytes);
|
|
648
|
+
this.#L.debug("received", decryptedMessage);
|
|
649
|
+
if (SecretChats.is("decryptedMessageLayer", decryptedMessage)) {
|
|
650
|
+
const x = isCreator ? 0 : 1;
|
|
651
|
+
const rawOutSeqNo = (decryptedMessage.out_seq_no - x) / 2;
|
|
652
|
+
if (pendingKey) {
|
|
653
|
+
this.#installNewKey(state, state.toCommitAuthKey, state.toCommitAuthKeyId, state.toCommitAuthKeyId_, false, rawOutSeqNo);
|
|
654
|
+
++state.authKeyUseCount;
|
|
655
|
+
state.toCommitId = 0n;
|
|
656
|
+
state.toCommitAuthKey = new Uint8Array();
|
|
657
|
+
state.toCommitAuthKeyId = 0n;
|
|
658
|
+
state.toCommitAuthKeyId_ = new Uint8Array();
|
|
659
|
+
}
|
|
660
|
+
else if (equals(authKeyId, state.authKeyId_)) {
|
|
661
|
+
++state.authKeyUseCount;
|
|
662
|
+
if (state.previousAuthKey.byteLength !== 0) {
|
|
663
|
+
state.previousAuthKeyDiscardAfterSeqNo = rawOutSeqNo;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
await this.#checkGap(state.encryptedChat.id, decryptedMessage, update.message);
|
|
667
|
+
if (pendingKey && Api.is("encryptedChat", state.encryptedChat)) {
|
|
668
|
+
const noop = { _: "decryptedMessageActionNoop" };
|
|
669
|
+
await this.#sendMessage({ _: "decryptedMessageService", random_id: getRandomId(), action: noop }, state.encryptedChat, state.authKey, state.authKeyId_);
|
|
670
|
+
}
|
|
671
|
+
if (state.previousAuthKeyDiscardAfterSeqNo >= 0 && state.inSeqNo > state.previousAuthKeyDiscardAfterSeqNo) {
|
|
672
|
+
this.#clearPreviousKey(state);
|
|
673
|
+
}
|
|
674
|
+
await this.#maybeStartRekey(state);
|
|
675
|
+
}
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
canHandleUpdate(update) {
|
|
679
|
+
return Api.isOneOf(secretChatManagerUpdates, update);
|
|
680
|
+
}
|
|
681
|
+
async handleUpdate(update) {
|
|
682
|
+
if (Api.is("updateNewEncryptedMessage", update)) {
|
|
683
|
+
return await this.#processUpdateNewMessageEncrypted(update);
|
|
684
|
+
}
|
|
685
|
+
if (Api.is("encryptedChatDiscarded", update.chat)) {
|
|
686
|
+
const state = this.#states.get(update.chat.id);
|
|
687
|
+
if (state !== undefined) {
|
|
688
|
+
this.#states.delete(update.chat.id);
|
|
689
|
+
state.encryptedChat = update.chat;
|
|
690
|
+
await state.commit(this.#c.messageStorage.storage);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
const state = this.#getSecretChatState(update.chat.id);
|
|
695
|
+
if (Api.is("encryptedChat", update.chat)) {
|
|
696
|
+
const pending = state.pendingExponent;
|
|
697
|
+
if (pending !== 0n) {
|
|
698
|
+
const gB = intFromBytes(update.chat.g_a_or_b, { byteOrder: "big", isSigned: false });
|
|
699
|
+
if (!isGoodModExpFirst(gB, state.prime)) {
|
|
700
|
+
this.#L.debug("discarding secret chat", update.chat.id, "because an invalid g_b was received");
|
|
701
|
+
await this.#c.invoke({ _: "messages.discardEncryption", chat_id: update.chat.id });
|
|
702
|
+
state.pendingExponent = 0n;
|
|
703
|
+
throw new TypeError("Received invalid g_b.");
|
|
704
|
+
}
|
|
705
|
+
const authKey = intToBytes(modExp(gB, pending, state.prime), 256, { byteOrder: "big", isSigned: false });
|
|
706
|
+
const authKeyId_ = (await sha1(authKey)).subarray(-8);
|
|
707
|
+
const authKeyId = intFromBytes(authKeyId_);
|
|
708
|
+
if (authKeyId !== update.chat.key_fingerprint) {
|
|
709
|
+
this.#L.debug("discarding secret chat", update.chat.id, "because of key fingerprint mismatch");
|
|
710
|
+
await this.#c.invoke({ _: "messages.discardEncryption", chat_id: update.chat.id });
|
|
711
|
+
state.pendingExponent = 0n;
|
|
712
|
+
throw new TypeError("Secret chat key fingerprint mismatch.");
|
|
713
|
+
}
|
|
714
|
+
state.authKey = authKey;
|
|
715
|
+
state.authKeyId = authKeyId;
|
|
716
|
+
state.authKeyId_ = authKeyId_;
|
|
717
|
+
state.authKeyCreatedAt = Date.now();
|
|
718
|
+
state.authKeyUseCount = 0;
|
|
719
|
+
state.isAuthKeyUsed = false;
|
|
720
|
+
state.pendingExponent = 0n;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
state.encryptedChat = update.chat;
|
|
724
|
+
}
|
|
725
|
+
const secretChat = constructSecretChat(update.chat);
|
|
726
|
+
return { type: "secretChat", secretChat };
|
|
727
|
+
}
|
|
728
|
+
async #startRekey(encryptedChat, authKeyId, authKey) {
|
|
729
|
+
const state = this.#getSecretChatState(encryptedChat.id);
|
|
730
|
+
if (state.rekeyId !== 0n || state.toCommitId !== 0n || state.previousAuthKey.byteLength !== 0) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
let g_a_;
|
|
734
|
+
let a;
|
|
735
|
+
do {
|
|
736
|
+
a = getRandomInt(256, false);
|
|
737
|
+
g_a_ = modExp(BigInt(state.g), a, state.prime);
|
|
738
|
+
} while (!isGoodModExpFirst(g_a_, state.prime));
|
|
739
|
+
const exchange_id = getRandomId();
|
|
740
|
+
state.rekeyId = exchange_id;
|
|
741
|
+
state.rekeyA = a;
|
|
742
|
+
const g_a = intToBytes(g_a_, 256, { byteOrder: "big", isSigned: false });
|
|
743
|
+
const random_id = getRandomId();
|
|
744
|
+
const action = {
|
|
745
|
+
_: "decryptedMessageActionRequestKey",
|
|
746
|
+
exchange_id,
|
|
747
|
+
g_a,
|
|
748
|
+
};
|
|
749
|
+
try {
|
|
750
|
+
await this.#sendMessage({
|
|
751
|
+
_: "decryptedMessageService",
|
|
752
|
+
random_id,
|
|
753
|
+
action,
|
|
754
|
+
}, encryptedChat, authKey, authKeyId);
|
|
755
|
+
}
|
|
756
|
+
catch (err) {
|
|
757
|
+
this.#clearInitiatedRekey(state);
|
|
758
|
+
throw err;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async #maybeStartRekey(state) {
|
|
762
|
+
if (!Api.is("encryptedChat", state.encryptedChat) || state.rekeyId !== 0n || state.toCommitId !== 0n || state.previousAuthKey.byteLength !== 0) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (state.authKeyUseCount > 100 || state.isAuthKeyUsed && Date.now() - state.authKeyCreatedAt >= WEEK) {
|
|
766
|
+
await this.#startRekey(state.encryptedChat, state.authKeyId_, state.authKey);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
_a = SecretChatManager;
|