mirror of
https://github.com/danog/MadelineProto.git
synced 2024-11-30 09:58:59 +01:00
Secret chat refactoring
This commit is contained in:
parent
ba3216b9a3
commit
7f2931bf7b
@ -267,7 +267,7 @@ final class Connection
|
||||
/**
|
||||
* Connects to a telegram DC using the specified protocol, proxy and connection parameters.
|
||||
*/
|
||||
public function connect(): self
|
||||
private function connect(): self
|
||||
{
|
||||
if ($this->stream) {
|
||||
return $this;
|
||||
@ -335,7 +335,7 @@ final class Connection
|
||||
* @param string $method Method name
|
||||
* @param array $arguments Arguments
|
||||
*/
|
||||
private function methodAbstractions(string &$method, array &$arguments): ?DeferredFuture
|
||||
private function methodAbstractions(string &$method, array &$arguments): void
|
||||
{
|
||||
if ($method === 'messages.importChatInvite' && isset($arguments['hash']) && \is_string($arguments['hash']) && $r = Tools::parseLink($arguments['hash'])) {
|
||||
[$invite, $content] = $r;
|
||||
@ -425,8 +425,7 @@ final class Connection
|
||||
$arguments['message']['media']['size'] = $arguments['file']['size'];
|
||||
}
|
||||
}
|
||||
$arguments['queuePromise'] = new DeferredFuture;
|
||||
return $arguments['queuePromise'];
|
||||
return;
|
||||
} elseif (\in_array($method, ['messages.addChatUser', 'messages.deleteChatUser', 'messages.editChatAdmin', 'messages.editChatPhoto', 'messages.editChatTitle', 'messages.getFullChat', 'messages.exportChatInvite', 'messages.editChatAdmin', 'messages.migrateChat'], true) && isset($arguments['chat_id']) && (!\is_numeric($arguments['chat_id']) || $arguments['chat_id'] < 0)) {
|
||||
$res = $this->API->getInfo($arguments['chat_id']);
|
||||
if ($res['type'] !== 'chat') {
|
||||
@ -463,10 +462,6 @@ final class Connection
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($method === 'messages.sendEncrypted' || $method === 'messages.sendEncryptedService') {
|
||||
$arguments['queuePromise'] = new DeferredFuture;
|
||||
return $arguments['queuePromise'];
|
||||
}
|
||||
if (isset($arguments['reply_to_msg_id'])) {
|
||||
if (isset($arguments['reply_to'])) {
|
||||
throw new Exception("You can't provide a reply_to together with reply_to_msg_id and top_msg_id!");
|
||||
@ -477,7 +472,6 @@ final class Connection
|
||||
'top_msg_id' => $arguments['top_msg_id'] ?? null
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Send an MTProto message.
|
||||
@ -499,7 +493,11 @@ final class Connection
|
||||
}
|
||||
if ($message->isMethod()) {
|
||||
$method = $message->getConstructor();
|
||||
$queuePromise = $this->methodAbstractions($method, $body);
|
||||
$this->methodAbstractions($method, $body);
|
||||
if (\in_array($method, ['messages.sendEncrypted', 'messages.sendEncryptedFile', 'messages.sendEncryptedService'], true)) {
|
||||
$id = $this->API->getSecretChat($body['peer'])['chat_id'];
|
||||
$body = $this->API->secret_chats[$id]->encryptSecretMessage($message);
|
||||
}
|
||||
$body = $this->API->getTL()->serializeMethod($method, $body);
|
||||
} else {
|
||||
$body['_'] = $message->getConstructor();
|
||||
@ -512,9 +510,6 @@ final class Connection
|
||||
unset($body);
|
||||
}
|
||||
$this->pendingOutgoing[$this->pendingOutgoingKey++] = $message;
|
||||
if (isset($queuePromise)) {
|
||||
$queuePromise->complete();
|
||||
}
|
||||
if ($flush && isset($this->writer)) {
|
||||
$this->writer->resume();
|
||||
}
|
||||
|
@ -564,7 +564,7 @@ final class DataCenterConnection implements JsonSerializable
|
||||
*/
|
||||
private function getAuthConnection(): Connection
|
||||
{
|
||||
return $this->connections[0]->connect();
|
||||
return $this->connections[0];
|
||||
}
|
||||
/**
|
||||
* Check if any connection is available.
|
||||
@ -583,7 +583,7 @@ final class DataCenterConnection implements JsonSerializable
|
||||
if (empty($this->availableConnections)) {
|
||||
$this->connectionsPromise->await();
|
||||
}
|
||||
return $this->getConnection()->connect();
|
||||
return $this->getConnection();
|
||||
}
|
||||
/**
|
||||
* Get best socket in round robin.
|
||||
|
@ -217,14 +217,19 @@ final class WriteLoop extends Loop
|
||||
}
|
||||
} elseif ($message->hasQueue()) {
|
||||
$queueId = $message->getQueueId();
|
||||
$this->connection->call_queue[$queueId] ??= [];
|
||||
$MTmessage['body'] = ($this->API->getTL()->serializeMethod('invokeAfterMsgs', ['msg_ids' => $this->connection->call_queue[$queueId], 'query' => $MTmessage['body']]));
|
||||
$this->connection->call_queue[$queueId][$message_id] = $message_id;
|
||||
if (\count($this->connection->call_queue[$queueId]) > $this->API->settings->getRpc()->getLimitCallQueue()) {
|
||||
\reset($this->connection->call_queue[$queueId]);
|
||||
$key = \key($this->connection->call_queue[$queueId]);
|
||||
unset($this->connection->call_queue[$queueId][$key]);
|
||||
if (isset($this->connection->callQueue[$queueId])) {
|
||||
$this->logger->logger("Adding $message to queue with ID $queueId", Logger::ULTRA_VERBOSE);
|
||||
$MTmessage['body'] = $this->API->getTL()->serializeMethod(
|
||||
'invokeAfterMsg',
|
||||
[
|
||||
'msg_id' => $this->connection->callQueue[$queueId],
|
||||
'query' => $MTmessage['body']
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$this->logger->logger("$message is the first in the queue with ID $queueId", Logger::ULTRA_VERBOSE);
|
||||
}
|
||||
$this->connection->callQueue[$queueId] = $message_id;
|
||||
}
|
||||
// TODO
|
||||
/*
|
||||
|
@ -18,11 +18,13 @@ declare(strict_types=1);
|
||||
* @link https://docs.madelineproto.xyz MadelineProto documentation
|
||||
*/
|
||||
|
||||
namespace danog\MadelineProto\Loop\Update;
|
||||
namespace danog\MadelineProto\Loop\Secret;
|
||||
|
||||
use danog\Loop\Loop;
|
||||
use danog\MadelineProto\Loop\InternalLoop;
|
||||
use danog\MadelineProto\MTProto;
|
||||
use danog\MadelineProto\SecretChats\SecretChat;
|
||||
use danog\MadelineProto\SecretChats\SecretChatController;
|
||||
use danog\MadelineProto\SecurityException;
|
||||
|
||||
/**
|
||||
@ -41,43 +43,32 @@ final class SecretFeedLoop extends Loop
|
||||
* Incoming secret updates array.
|
||||
*/
|
||||
private array $incomingUpdates = [];
|
||||
/**
|
||||
* Secret chat ID.
|
||||
*/
|
||||
private int $secretId;
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MTProto $API API instance
|
||||
* @param integer $secretId Secret chat ID
|
||||
*/
|
||||
public function __construct(MTProto $API, int $secretId)
|
||||
public function __construct(MTProto $API, private readonly SecretChatController $secretChat)
|
||||
{
|
||||
$this->init($API);
|
||||
$this->secretId = $secretId;
|
||||
}
|
||||
/**
|
||||
* Main loop.
|
||||
*/
|
||||
public function loop(): ?float
|
||||
{
|
||||
if (!$this->isLoggedIn()) {
|
||||
return self::PAUSE;
|
||||
}
|
||||
$this->logger->logger("Resumed {$this}");
|
||||
while ($this->incomingUpdates) {
|
||||
$updates = $this->incomingUpdates;
|
||||
$this->incomingUpdates = [];
|
||||
foreach ($updates as $update) {
|
||||
try {
|
||||
if (!$this->API->handleEncryptedUpdate($update)) {
|
||||
if (!$this->secretChat->handleEncryptedUpdate($update)) {
|
||||
$this->logger->logger("Secret chat deleted, exiting $this...");
|
||||
unset($this->API->secretFeeders[$this->secretId]);
|
||||
return self::STOP;
|
||||
}
|
||||
} catch (SecurityException $e) {
|
||||
$this->logger->logger("Secret chat deleted, exiting $this...");
|
||||
unset($this->API->secretFeeders[$this->secretId]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
@ -94,6 +85,6 @@ final class SecretFeedLoop extends Loop
|
||||
}
|
||||
public function __toString(): string
|
||||
{
|
||||
return "secret chat feed loop {$this->secretId}";
|
||||
return "secret chat feed loop {$this->secretChat->public->chatId}";
|
||||
}
|
||||
}
|
@ -37,7 +37,7 @@ use danog\MadelineProto\EventHandler\Message;
|
||||
use danog\MadelineProto\Ipc\Server;
|
||||
use danog\MadelineProto\Loop\Generic\PeriodicLoopInternal;
|
||||
use danog\MadelineProto\Loop\Update\FeedLoop;
|
||||
use danog\MadelineProto\Loop\Update\SecretFeedLoop;
|
||||
use danog\MadelineProto\Loop\Secret\SecretFeedLoop;
|
||||
use danog\MadelineProto\Loop\Update\SeqLoop;
|
||||
use danog\MadelineProto\Loop\Update\UpdateLoop;
|
||||
use danog\MadelineProto\MTProtoTools\AuthKeyHandler;
|
||||
@ -97,9 +97,6 @@ final class MTProto implements TLCallback, LoggerGetter
|
||||
use UpdateHandler;
|
||||
use Files;
|
||||
use \danog\MadelineProto\SecretChats\AuthKeyHandler;
|
||||
use MessageHandler;
|
||||
use ResponseHandler;
|
||||
use SeqNoHandler;
|
||||
use BotAPI;
|
||||
use BotAPIFiles;
|
||||
use TD;
|
||||
@ -123,7 +120,7 @@ final class MTProto implements TLCallback, LoggerGetter
|
||||
* @internal
|
||||
* @var int
|
||||
*/
|
||||
const V = 172;
|
||||
const V = 173;
|
||||
/**
|
||||
* Bad message error codes.
|
||||
*
|
||||
@ -303,12 +300,6 @@ final class MTProto implements TLCallback, LoggerGetter
|
||||
* @var array<FeedLoop>
|
||||
*/
|
||||
public array $feeders = [];
|
||||
/**
|
||||
* Secret chat feeder loops.
|
||||
*
|
||||
* @var array<SecretFeedLoop>
|
||||
*/
|
||||
public array $secretFeeders = [];
|
||||
/**
|
||||
* Updater loops.
|
||||
*
|
||||
@ -881,7 +872,8 @@ final class MTProto implements TLCallback, LoggerGetter
|
||||
$this->config = ['expires' => -1];
|
||||
$this->dh_config = ['version' => 0];
|
||||
$this->initialize($this->settings);
|
||||
foreach ($this->secret_chats as $chat => $data) {
|
||||
foreach ($this->secret_chats as $chat => &$data) {
|
||||
$data['chat_id'] = $chat;
|
||||
try {
|
||||
if (isset($this->secret_chats[$chat]) && $this->secret_chats[$chat]['InputEncryptedChat'] !== null) {
|
||||
$this->notifyLayer($chat);
|
||||
@ -1237,15 +1229,6 @@ final class MTProto implements TLCallback, LoggerGetter
|
||||
return;
|
||||
}
|
||||
$this->logger('Starting update system');
|
||||
foreach ($this->secret_chats as $id => $chat) {
|
||||
if (!isset($this->secretFeeders[$id])) {
|
||||
$this->secretFeeders[$id] = new SecretFeedLoop($this, $id);
|
||||
}
|
||||
$this->secretFeeders[$id]->start();
|
||||
if (isset($this->secretFeeders[$id])) {
|
||||
$this->secretFeeders[$id]->resume();
|
||||
}
|
||||
}
|
||||
$this->channels_state->get(FeedLoop::GENERIC);
|
||||
$channelIds = [];
|
||||
foreach ($this->channels_state->get() as $state) {
|
||||
|
@ -120,9 +120,6 @@ trait CallHandler
|
||||
$aargs['datacenter'] = -$this->datacenter;
|
||||
return $this->API->methodCallAsyncWrite($method, $args, $aargs);
|
||||
}
|
||||
if (\in_array($method, ['messages.setEncryptedTyping', 'messages.readEncryptedHistory', 'messages.sendEncrypted', 'messages.sendEncryptedFile', 'messages.sendEncryptedService', 'messages.receivedQueue'], true)) {
|
||||
$aargs['queue'] = 'secret';
|
||||
}
|
||||
if (\is_array($args)) {
|
||||
if (isset($args['multiple'])) {
|
||||
$aargs['multiple'] = true;
|
||||
@ -130,7 +127,7 @@ trait CallHandler
|
||||
if (isset($args['message']) && \is_string($args['message']) && \mb_strlen($args['message'], 'UTF-8') > ($this->API->getConfig())['message_length_max'] && \mb_strlen($this->API->parseMode($args)['message'], 'UTF-8') > ($this->API->getConfig())['message_length_max']) {
|
||||
$args = $this->API->splitToChunks($args);
|
||||
$promises = [];
|
||||
$aargs['queue'] = $method.' '.\time();
|
||||
$aargs['queue'] = $method.' '.$this->API->getId($args['peer']);
|
||||
$aargs['multiple'] = true;
|
||||
}
|
||||
if (isset($aargs['multiple'])) {
|
||||
|
@ -291,6 +291,15 @@ trait ResponseHandler
|
||||
case 500:
|
||||
case -500:
|
||||
case -503:
|
||||
if ($request->hasQueue() &&
|
||||
(
|
||||
$response['error_message'] === 'MSG_WAIT_FAILED'
|
||||
|| $response['error_message'] === 'MSG_WAIT_TIMEOUT'
|
||||
)
|
||||
) {
|
||||
EventLoop::delay(1.0, fn () => $this->methodRecall(message_id: $request->getMsgId()));
|
||||
return null;
|
||||
}
|
||||
if ((($response['error_code'] === -503 || $response['error_message'] === '-503') && !\in_array($request->getConstructor(), ['messages.getBotCallbackAnswer', 'messages.getInlineBotResults'], true))
|
||||
|| (\in_array($response['error_message'], ['MSGID_DECREASE_RETRY', 'HISTORY_GET_FAILED', 'RPC_CONNECT_FAILED', 'RPC_CALL_FAIL', 'RPC_MCGET_FAIL', 'PERSISTENT_TIMESTAMP_OUTDATED', 'RPC_MCGET_FAIL', 'no workers running', 'No workers running'], true))) {
|
||||
EventLoop::delay(1.0, fn () => $this->methodRecall(message_id: $request->getMsgId()));
|
||||
|
@ -81,8 +81,9 @@ trait Session
|
||||
/**
|
||||
* Call queue.
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
public array $call_queue = [];
|
||||
public array $callQueue = [];
|
||||
/**
|
||||
* Ack queue.
|
||||
*
|
||||
|
@ -21,9 +21,10 @@ declare(strict_types=1);
|
||||
namespace danog\MadelineProto\SecretChats;
|
||||
|
||||
use danog\MadelineProto\Logger;
|
||||
use danog\MadelineProto\Loop\Update\SecretFeedLoop;
|
||||
use danog\MadelineProto\Loop\Secret\SecretFeedLoop;
|
||||
use danog\MadelineProto\Loop\Update\UpdateLoop;
|
||||
use danog\MadelineProto\MTProtoTools\Crypt;
|
||||
use danog\MadelineProto\MTProtoTools\DialogId;
|
||||
use danog\MadelineProto\PeerNotInDbException;
|
||||
use danog\MadelineProto\RPCErrorException;
|
||||
use danog\MadelineProto\SecurityException;
|
||||
@ -49,64 +50,9 @@ trait AuthKeyHandler
|
||||
/**
|
||||
* Secret chats.
|
||||
*
|
||||
* @var array<int, SecretChatController>
|
||||
*/
|
||||
protected array $secret_chats = [];
|
||||
/**
|
||||
* Accept secret chat.
|
||||
*
|
||||
* @param array $params Secret chat ID
|
||||
*/
|
||||
public function acceptSecretChat(array $params): void
|
||||
{
|
||||
//$this->logger->logger($params['id'],$this->secretChatStatus($params['id']));
|
||||
if ($this->secretChatStatus($params['id']) !== 0) {
|
||||
//$this->logger->logger($this->secretChatStatus($params['id']));
|
||||
$this->logger->logger("I've already accepted secret chat ".$params['id']);
|
||||
return;
|
||||
}
|
||||
$dh_config = ($this->getDhConfig());
|
||||
$this->logger->logger('Generating b...', Logger::VERBOSE);
|
||||
$b = new BigInteger(Tools::random(256), 256);
|
||||
$params['g_a'] = new BigInteger((string) $params['g_a'], 256);
|
||||
Crypt::checkG($params['g_a'], $dh_config['p']);
|
||||
$key = ['auth_key' => \str_pad($params['g_a']->powMod($b, $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT)];
|
||||
//$this->logger->logger($key);
|
||||
$key['fingerprint'] = \substr(\sha1($key['auth_key'], true), -8);
|
||||
$key['visualization_orig'] = \substr(\sha1($key['auth_key'], true), 16);
|
||||
$key['visualization_46'] = \substr(\hash('sha256', $key['auth_key'], true), 20);
|
||||
$this->secret_chats[$params['id']] = [
|
||||
'key' => $key,
|
||||
'admin' => false,
|
||||
'user_id' => $params['admin_id'],
|
||||
'InputEncryptedChat' => [
|
||||
'_' => 'inputEncryptedChat',
|
||||
'chat_id' => $params['id'],
|
||||
'access_hash' => $params['access_hash'],
|
||||
],
|
||||
'in_seq_no_x' => 1,
|
||||
'out_seq_no_x' => 0,
|
||||
'in_seq_no' => 0,
|
||||
'out_seq_no' => 0,
|
||||
'layer' => 8,
|
||||
'ttl' => 0,
|
||||
'ttr' => 100,
|
||||
'updated' => \time(),
|
||||
'incoming' => [],
|
||||
'outgoing' => [],
|
||||
'created' => \time(),
|
||||
'rekeying' => [0],
|
||||
'key_x' => 'from server',
|
||||
'mtproto' => 1,
|
||||
];
|
||||
$this->secretFeeders[$params['id']] = new SecretFeedLoop($this, $params['id']);
|
||||
$this->secretFeeders[$params['id']]->start();
|
||||
$this->secretFeeders[$params['id']]->resume();
|
||||
$g_b = $dh_config['g']->powMod($b, $dh_config['p']);
|
||||
Crypt::checkG($g_b, $dh_config['p']);
|
||||
$this->methodCallAsyncRead('messages.acceptEncryption', ['peer' => $params['id'], 'g_b' => $g_b->toBytes(), 'key_fingerprint' => $key['fingerprint']]);
|
||||
$this->notifyLayer($params['id']);
|
||||
$this->logger->logger('Secret chat '.$params['id'].' accepted successfully!', Logger::NOTICE);
|
||||
}
|
||||
public array $secret_chats = [];
|
||||
/**
|
||||
* Request secret chat.
|
||||
*
|
||||
@ -132,6 +78,65 @@ trait AuthKeyHandler
|
||||
$this->logger->logger('Secret chat '.$res['id'].' requested successfully!', Logger::NOTICE);
|
||||
return $res['id'];
|
||||
}
|
||||
/**
|
||||
* Accept secret chat.
|
||||
*
|
||||
* @param array $params Secret chat ID
|
||||
*/
|
||||
public function acceptSecretChat(array $params): void
|
||||
{
|
||||
if ($this->secretChatStatus($params['id']) !== 0) {
|
||||
$this->logger->logger("I've already accepted secret chat ".$params['id']);
|
||||
return;
|
||||
}
|
||||
$dh_config = $this->getDhConfig();
|
||||
$this->logger->logger('Generating b...', Logger::VERBOSE);
|
||||
$b = new BigInteger(Tools::random(256), 256);
|
||||
$params['g_a'] = new BigInteger((string) $params['g_a'], 256);
|
||||
Crypt::checkG($params['g_a'], $dh_config['p']);
|
||||
$key = ['auth_key' => \str_pad($params['g_a']->powMod($b, $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT)];
|
||||
//$this->logger->logger($key);
|
||||
$key['fingerprint'] = \substr(\sha1($key['auth_key'], true), -8);
|
||||
$key['visualization_orig'] = \substr(\sha1($key['auth_key'], true), 16);
|
||||
$key['visualization_46'] = \substr(\hash('sha256', $key['auth_key'], true), 20);
|
||||
$this->secret_chats[$params['id']] = new SecretChatController(
|
||||
$this,
|
||||
new SecretChat(
|
||||
DialogId::fromSecretChatId($params['id']),
|
||||
false,
|
||||
$params['admin_id'],
|
||||
time(),
|
||||
0
|
||||
)
|
||||
) [
|
||||
'chat_id' => $params['id'],
|
||||
'key' => $key,
|
||||
'admin' => false,
|
||||
'user_id' => $params['admin_id'],
|
||||
'InputEncryptedChat' => [
|
||||
'_' => 'inputEncryptedChat',
|
||||
'chat_id' => $params['id'],
|
||||
'access_hash' => $params['access_hash'],
|
||||
],
|
||||
'in_seq_no_x' => 1,
|
||||
'out_seq_no_x' => 0,
|
||||
'in_seq_no' => 0,
|
||||
'out_seq_no' => 0,
|
||||
'layer' => 8,
|
||||
'ttl' => 0,
|
||||
'ttr' => 100,
|
||||
'updated' => \time(),
|
||||
'incoming' => [],
|
||||
'outgoing' => [],
|
||||
'created' => \time(),
|
||||
'mtproto' => 1,
|
||||
];
|
||||
$g_b = $dh_config['g']->powMod($b, $dh_config['p']);
|
||||
Crypt::checkG($g_b, $dh_config['p']);
|
||||
$this->methodCallAsyncRead('messages.acceptEncryption', ['peer' => $params['id'], 'g_b' => $g_b->toBytes(), 'key_fingerprint' => $key['fingerprint']]);
|
||||
$this->notifyLayer($params['id']);
|
||||
$this->logger->logger('Secret chat '.$params['id'].' accepted successfully!', Logger::NOTICE);
|
||||
}
|
||||
/**
|
||||
* Complete secret chat.
|
||||
*
|
||||
@ -157,166 +162,34 @@ trait AuthKeyHandler
|
||||
}
|
||||
$key['visualization_orig'] = \substr(\sha1($key['auth_key'], true), 16);
|
||||
$key['visualization_46'] = \substr(\hash('sha256', $key['auth_key'], true), 20);
|
||||
$this->secret_chats[$params['id']] = ['key' => $key, 'admin' => true, 'user_id' => $params['participant_id'], 'InputEncryptedChat' => ['chat_id' => $params['id'], 'access_hash' => $params['access_hash'], '_' => 'inputEncryptedChat'], 'in_seq_no_x' => 0, 'out_seq_no_x' => 1, 'in_seq_no' => 0, 'out_seq_no' => 0, 'layer' => 8, 'ttl' => 0, 'ttr' => 100, 'updated' => \time(), 'incoming' => [], 'outgoing' => [], 'created' => \time(), 'rekeying' => [0], 'key_x' => 'to server', 'mtproto' => 1];
|
||||
$this->secretFeeders[$params['id']] = new SecretFeedLoop($this, $params['id']);
|
||||
$this->secretFeeders[$params['id']]->start();
|
||||
$this->secretFeeders[$params['id']]->resume();
|
||||
$this->notifyLayer($params['id']);
|
||||
$this->secret_chats[$params['id']] = $chat = new SecretChatController(
|
||||
$this,
|
||||
new SecretChat(
|
||||
DialogId::fromSecretChatId($params['id']),
|
||||
true,
|
||||
$params['participant_id'],
|
||||
time(),
|
||||
0
|
||||
),
|
||||
1,
|
||||
0
|
||||
);
|
||||
$chat->notifyLayer();
|
||||
$this->logger->logger('Secret chat '.$params['id'].' completed successfully!', Logger::NOTICE);
|
||||
}
|
||||
private function notifyLayer($chat): void
|
||||
{
|
||||
$this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionNotifyLayer', 'layer' => $this->TL->getSecretLayer()]]]);
|
||||
}
|
||||
/**
|
||||
* Temporary rekeyed secret chats.
|
||||
*
|
||||
*/
|
||||
protected array $temp_rekeyed_secret_chats = [];
|
||||
/**
|
||||
* Rekey secret chat.
|
||||
*
|
||||
* @param int $chat Secret chat to rekey
|
||||
*/
|
||||
public function rekey(int $chat): ?string
|
||||
{
|
||||
if ($this->secret_chats[$chat]['rekeying'][0] !== 0) {
|
||||
return null;
|
||||
}
|
||||
$this->logger->logger('Rekeying secret chat '.$chat.'...', Logger::VERBOSE);
|
||||
$dh_config = ($this->getDhConfig());
|
||||
$this->logger->logger('Generating a...', Logger::VERBOSE);
|
||||
$a = new BigInteger(Tools::random(256), 256);
|
||||
$this->logger->logger('Generating g_a...', Logger::VERBOSE);
|
||||
$g_a = $dh_config['g']->powMod($a, $dh_config['p']);
|
||||
Crypt::checkG($g_a, $dh_config['p']);
|
||||
$e = Tools::random(8);
|
||||
$this->temp_rekeyed_secret_chats[$e] = $a;
|
||||
$this->secret_chats[$chat]['rekeying'] = [1, $e];
|
||||
$this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionRequestKey', 'g_a' => $g_a->toBytes(), 'exchange_id' => $e]]]);
|
||||
$this->updaters[UpdateLoop::GENERIC]->resume();
|
||||
return $e;
|
||||
}
|
||||
/**
|
||||
* Accept rekeying.
|
||||
*
|
||||
* @param int $chat Chat
|
||||
* @param array $params Parameters
|
||||
*/
|
||||
private function acceptRekey(int $chat, array $params): void
|
||||
{
|
||||
if ($this->secret_chats[$chat]['rekeying'][0] !== 0) {
|
||||
$my_exchange_id = new BigInteger($this->secret_chats[$chat]['rekeying'][1], -256);
|
||||
$other_exchange_id = new BigInteger($params['exchange_id'], -256);
|
||||
//$this->logger->logger($my, $params);
|
||||
if ($my_exchange_id->compare($other_exchange_id) > 0) {
|
||||
return;
|
||||
}
|
||||
if ($my_exchange_id->compare($other_exchange_id) === 0) {
|
||||
$this->secret_chats[$chat]['rekeying'] = [0];
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->logger->logger('Accepting rekeying of secret chat '.$chat.'...', Logger::VERBOSE);
|
||||
$dh_config = ($this->getDhConfig());
|
||||
$this->logger->logger('Generating b...', Logger::VERBOSE);
|
||||
$b = new BigInteger(Tools::random(256), 256);
|
||||
$params['g_a'] = new BigInteger((string) $params['g_a'], 256);
|
||||
Crypt::checkG($params['g_a'], $dh_config['p']);
|
||||
$key = ['auth_key' => \str_pad($params['g_a']->powMod($b, $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT)];
|
||||
$key['fingerprint'] = \substr(\sha1($key['auth_key'], true), -8);
|
||||
$key['visualization_orig'] = $this->secret_chats[$chat]['key']['visualization_orig'];
|
||||
$key['visualization_46'] = \substr(\hash('sha256', $key['auth_key'], true), 20);
|
||||
$this->temp_rekeyed_secret_chats[$params['exchange_id']] = $key;
|
||||
$this->secret_chats[$chat]['rekeying'] = [2, $params['exchange_id']];
|
||||
$g_b = $dh_config['g']->powMod($b, $dh_config['p']);
|
||||
Crypt::checkG($g_b, $dh_config['p']);
|
||||
$this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionAcceptKey', 'g_b' => $g_b->toBytes(), 'exchange_id' => $params['exchange_id'], 'key_fingerprint' => $key['fingerprint']]]]);
|
||||
$this->updaters[UpdateLoop::GENERIC]->resume();
|
||||
}
|
||||
/**
|
||||
* Commit rekeying of secret chat.
|
||||
*
|
||||
* @param int $chat Chat
|
||||
* @param array $params Parameters
|
||||
*/
|
||||
private function commitRekey(int $chat, array $params): void
|
||||
{
|
||||
if ($this->secret_chats[$chat]['rekeying'][0] !== 1 || !isset($this->temp_rekeyed_secret_chats[$params['exchange_id']])) {
|
||||
$this->secret_chats[$chat]['rekeying'] = [0];
|
||||
return;
|
||||
}
|
||||
$this->logger->logger('Committing rekeying of secret chat '.$chat.'...', Logger::VERBOSE);
|
||||
$dh_config = ($this->getDhConfig());
|
||||
$params['g_b'] = new BigInteger((string) $params['g_b'], 256);
|
||||
Crypt::checkG($params['g_b'], $dh_config['p']);
|
||||
$key = ['auth_key' => \str_pad($params['g_b']->powMod($this->temp_rekeyed_secret_chats[$params['exchange_id']], $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT)];
|
||||
$key['fingerprint'] = \substr(\sha1($key['auth_key'], true), -8);
|
||||
$key['visualization_orig'] = $this->secret_chats[$chat]['key']['visualization_orig'];
|
||||
$key['visualization_46'] = \substr(\hash('sha256', $key['auth_key'], true), 20);
|
||||
if ($key['fingerprint'] !== $params['key_fingerprint']) {
|
||||
$this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionAbortKey', 'exchange_id' => $params['exchange_id']]]]);
|
||||
throw new SecurityException('Invalid key fingerprint!');
|
||||
}
|
||||
$this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionCommitKey', 'exchange_id' => $params['exchange_id'], 'key_fingerprint' => $key['fingerprint']]]]);
|
||||
unset($this->temp_rekeyed_secret_chats[$params['exchange_id']]);
|
||||
$this->secret_chats[$chat]['rekeying'] = [0];
|
||||
$this->secret_chats[$chat]['old_key'] = $this->secret_chats[$chat]['key'];
|
||||
$this->secret_chats[$chat]['key'] = $key;
|
||||
$this->secret_chats[$chat]['ttr'] = 100;
|
||||
$this->secret_chats[$chat]['updated'] = \time();
|
||||
$this->updaters[UpdateLoop::GENERIC]->resume();
|
||||
}
|
||||
/**
|
||||
* Complete rekeying.
|
||||
*
|
||||
* @param int $chat Chat
|
||||
* @param array $params Parameters
|
||||
*/
|
||||
private function completeRekey(int $chat, array $params): bool
|
||||
{
|
||||
if ($this->secret_chats[$chat]['rekeying'][0] !== 2 || !isset($this->temp_rekeyed_secret_chats[$params['exchange_id']]['fingerprint'])) {
|
||||
return false;
|
||||
}
|
||||
if ($this->temp_rekeyed_secret_chats[$params['exchange_id']]['fingerprint'] !== $params['key_fingerprint']) {
|
||||
$this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionAbortKey', 'exchange_id' => $params['exchange_id']]]]);
|
||||
throw new SecurityException('Invalid key fingerprint!');
|
||||
}
|
||||
$this->logger->logger('Completing rekeying of secret chat '.$chat.'...', Logger::VERBOSE);
|
||||
$this->secret_chats[$chat]['rekeying'] = [0];
|
||||
$this->secret_chats[$chat]['old_key'] = $this->secret_chats[$chat]['key'];
|
||||
$this->secret_chats[$chat]['key'] = $this->temp_rekeyed_secret_chats[$params['exchange_id']];
|
||||
$this->secret_chats[$chat]['ttr'] = 100;
|
||||
$this->secret_chats[$chat]['updated'] = \time();
|
||||
unset($this->temp_rekeyed_secret_chats[$params['exchange_id']]);
|
||||
$this->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $chat, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionNoop']]]);
|
||||
$this->logger->logger('Secret chat '.$chat.' rekeyed successfully!', Logger::VERBOSE);
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Get secret chat status.
|
||||
*
|
||||
* @param int $chat Chat ID
|
||||
* @return int One of \danog\MadelineProto\API::SECRET_EMPTY, \danog\MadelineProto\API::SECRET_REQUESTED, \danog\MadelineProto\API::SECRET_READY
|
||||
*/
|
||||
public function secretChatStatus(int $chat): int
|
||||
{
|
||||
if (isset($this->secret_chats[$chat])) {
|
||||
return \danog\MadelineProto\API::SECRET_READY;
|
||||
}
|
||||
if (isset($this->temp_requested_secret_chats[$chat])) {
|
||||
return \danog\MadelineProto\API::SECRET_REQUESTED;
|
||||
}
|
||||
return \danog\MadelineProto\API::SECRET_EMPTY;
|
||||
}
|
||||
/**
|
||||
* Get secret chat.
|
||||
*
|
||||
* @param array|int $chat Secret chat ID
|
||||
*/
|
||||
public function getSecretChat(array|int $chat): array
|
||||
public function getSecretChat(array|int $chat): SecretChat
|
||||
{
|
||||
return $this->secret_chats[\is_array($chat) ? $chat['chat_id'] : $chat];
|
||||
if (is_array($chat)) {
|
||||
return $this->getInfo($chat);
|
||||
} elseif (DialogId::isSecretChat($chat)) {
|
||||
$chat = DialogId::toSecretChatId($chat);
|
||||
}
|
||||
return $this->secret_chats[$chat];
|
||||
}
|
||||
/**
|
||||
* Check whether secret chat exists.
|
||||
|
@ -1,201 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* MessageHandler module.
|
||||
*
|
||||
* This file is part of MadelineProto.
|
||||
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU Affero General Public License for more details.
|
||||
* You should have received a copy of the GNU General Public License along with MadelineProto.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
|
||||
* @link https://docs.madelineproto.xyz MadelineProto documentation
|
||||
*/
|
||||
|
||||
namespace danog\MadelineProto\SecretChats;
|
||||
|
||||
use Amp\DeferredFuture;
|
||||
use Amp\Future;
|
||||
use danog\MadelineProto\Lang;
|
||||
use danog\MadelineProto\Logger;
|
||||
use danog\MadelineProto\MTProtoTools\Crypt;
|
||||
use danog\MadelineProto\SecurityException;
|
||||
use danog\MadelineProto\Tools;
|
||||
|
||||
/**
|
||||
* Manages packing and unpacking of messages, and the list of sent and received messages.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait MessageHandler
|
||||
{
|
||||
/**
|
||||
* Secret queue.
|
||||
*
|
||||
* @var array<Future<null>>
|
||||
*/
|
||||
private array $secretQueue = [];
|
||||
/**
|
||||
* Encrypt secret chat message.
|
||||
*
|
||||
* @param integer $chat_id Chat ID
|
||||
* @param array $message Message to encrypt
|
||||
* @param DeferredFuture<null> $queuePromise Queue promise
|
||||
* @internal
|
||||
*/
|
||||
public function encryptSecretMessage(int $chat_id, array $message, DeferredFuture $queuePromise): string|false
|
||||
{
|
||||
if (!isset($this->secret_chats[$chat_id])) {
|
||||
$this->logger->logger(\sprintf(Lang::$current_lang['secret_chat_skipping'], $chat_id));
|
||||
return false;
|
||||
}
|
||||
$message['random_id'] = Tools::random(8);
|
||||
$this->secret_chats[$chat_id]['ttr']--;
|
||||
if ($this->secret_chats[$chat_id]['layer'] > 8) {
|
||||
if (($this->secret_chats[$chat_id]['ttr'] <= 0 || \time() - $this->secret_chats[$chat_id]['updated'] > 7 * 24 * 60 * 60) && $this->secret_chats[$chat_id]['rekeying'][0] === 0) {
|
||||
$this->rekey($chat_id);
|
||||
}
|
||||
if (isset($this->secretQueue[$chat_id])) {
|
||||
$promise = $this->secretQueue[$chat_id];
|
||||
$this->secretQueue[$chat_id] = $queuePromise->getFuture();
|
||||
$promise->await();
|
||||
} else {
|
||||
$this->secretQueue[$chat_id] = $queuePromise->getFuture();
|
||||
}
|
||||
$message = ['_' => 'decryptedMessageLayer', 'layer' => $this->secret_chats[$chat_id]['layer'], 'in_seq_no' => $this->generateSecretInSeqNo($chat_id), 'out_seq_no' => $this->generateSecretOutSeqNo($chat_id), 'message' => $message];
|
||||
$this->secret_chats[$chat_id]['out_seq_no']++;
|
||||
}
|
||||
$this->secret_chats[$chat_id]['outgoing'][$this->secret_chats[$chat_id]['out_seq_no']] = $message;
|
||||
$constructor = $this->secret_chats[$chat_id]['layer'] === 8 ? 'DecryptedMessage' : 'DecryptedMessageLayer';
|
||||
$message = $this->TL->serializeObject(['type' => $constructor], $message, $constructor, $this->secret_chats[$chat_id]['layer']);
|
||||
$message = Tools::packUnsignedInt(\strlen($message)).$message;
|
||||
if ($this->secret_chats[$chat_id]['mtproto'] === 2) {
|
||||
$padding = Tools::posmod(-\strlen($message), 16);
|
||||
if ($padding < 12) {
|
||||
$padding += 16;
|
||||
}
|
||||
$message .= Tools::random($padding);
|
||||
$message_key = \substr(\hash('sha256', \substr($this->secret_chats[$chat_id]['key']['auth_key'], 88 + ($this->secret_chats[$chat_id]['admin'] ? 0 : 8), 32).$message, true), 8, 16);
|
||||
[$aes_key, $aes_iv] = Crypt::kdf($message_key, $this->secret_chats[$chat_id]['key']['auth_key'], $this->secret_chats[$chat_id]['admin']);
|
||||
} else {
|
||||
$message_key = \substr(\sha1($message, true), -16);
|
||||
[$aes_key, $aes_iv] = Crypt::oldKdf($message_key, $this->secret_chats[$chat_id]['key']['auth_key'], true);
|
||||
$message .= Tools::random(Tools::posmod(-\strlen($message), 16));
|
||||
}
|
||||
$message = $this->secret_chats[$chat_id]['key']['fingerprint'].$message_key.Crypt::igeEncrypt($message, $aes_key, $aes_iv);
|
||||
return $message;
|
||||
}
|
||||
/**
|
||||
* Handle encrypted update.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function handleEncryptedUpdate(array $message): bool
|
||||
{
|
||||
if (!isset($this->secret_chats[$message['message']['chat_id']])) {
|
||||
$this->logger->logger(\sprintf(Lang::$current_lang['secret_chat_skipping'], $message['message']['chat_id']));
|
||||
return false;
|
||||
}
|
||||
$message['message']['bytes'] = (string) $message['message']['bytes'];
|
||||
$auth_key_id = \substr($message['message']['bytes'], 0, 8);
|
||||
$old = false;
|
||||
if ($auth_key_id !== $this->secret_chats[$message['message']['chat_id']]['key']['fingerprint']) {
|
||||
if (isset($this->secret_chats[$message['message']['chat_id']]['old_key']['fingerprint'])) {
|
||||
if ($auth_key_id !== $this->secret_chats[$message['message']['chat_id']]['old_key']['fingerprint']) {
|
||||
$this->discardSecretChat($message['message']['chat_id']);
|
||||
throw new SecurityException('Key fingerprint mismatch');
|
||||
}
|
||||
$old = true;
|
||||
} else {
|
||||
$this->discardSecretChat($message['message']['chat_id']);
|
||||
throw new SecurityException('Key fingerprint mismatch');
|
||||
}
|
||||
}
|
||||
$message_key = \substr($message['message']['bytes'], 8, 16);
|
||||
$encrypted_data = \substr($message['message']['bytes'], 24);
|
||||
if ($this->secret_chats[$message['message']['chat_id']]['mtproto'] === 2) {
|
||||
$this->logger->logger('Trying MTProto v2 decryption for chat '.$message['message']['chat_id'].'...', Logger::NOTICE);
|
||||
try {
|
||||
$message_data = $this->tryMTProtoV2Decrypt($message_key, $message['message']['chat_id'], $old, $encrypted_data);
|
||||
$this->logger->logger('MTProto v2 decryption OK for chat '.$message['message']['chat_id'].'...', Logger::NOTICE);
|
||||
} catch (SecurityException $e) {
|
||||
$this->logger->logger('MTProto v2 decryption failed with message '.$e->getMessage().', trying MTProto v1 decryption for chat '.$message['message']['chat_id'].'...', Logger::NOTICE);
|
||||
$message_data = $this->tryMTProtoV1Decrypt($message_key, $message['message']['chat_id'], $old, $encrypted_data);
|
||||
$this->logger->logger('MTProto v1 decryption OK for chat '.$message['message']['chat_id'].'...', Logger::NOTICE);
|
||||
$this->secret_chats[$message['message']['chat_id']]['mtproto'] = 1;
|
||||
}
|
||||
} else {
|
||||
$this->logger->logger('Trying MTProto v1 decryption for chat '.$message['message']['chat_id'].'...', Logger::NOTICE);
|
||||
try {
|
||||
$message_data = $this->tryMTProtoV1Decrypt($message_key, $message['message']['chat_id'], $old, $encrypted_data);
|
||||
$this->logger->logger('MTProto v1 decryption OK for chat '.$message['message']['chat_id'].'...', Logger::NOTICE);
|
||||
} catch (SecurityException $e) {
|
||||
$this->logger->logger('MTProto v1 decryption failed with message '.$e->getMessage().', trying MTProto v2 decryption for chat '.$message['message']['chat_id'].'...', Logger::NOTICE);
|
||||
$message_data = $this->tryMTProtoV2Decrypt($message_key, $message['message']['chat_id'], $old, $encrypted_data);
|
||||
$this->logger->logger('MTProto v2 decryption OK for chat '.$message['message']['chat_id'].'...', Logger::NOTICE);
|
||||
$this->secret_chats[$message['message']['chat_id']]['mtproto'] = 2;
|
||||
}
|
||||
}
|
||||
$deserialized = $this->TL->deserialize($message_data, ['type' => '']);
|
||||
$this->secret_chats[$message['message']['chat_id']]['ttr']--;
|
||||
if (($this->secret_chats[$message['message']['chat_id']]['ttr'] <= 0 || \time() - $this->secret_chats[$message['message']['chat_id']]['updated'] > 7 * 24 * 60 * 60) && $this->secret_chats[$message['message']['chat_id']]['rekeying'][0] === 0) {
|
||||
$this->rekey($message['message']['chat_id']);
|
||||
}
|
||||
unset($message['message']['bytes']);
|
||||
$message['message']['decrypted_message'] = $deserialized;
|
||||
$this->secret_chats[$message['message']['chat_id']]['incoming'][$this->secret_chats[$message['message']['chat_id']]['in_seq_no']] = $message['message'];
|
||||
$this->handleDecryptedUpdate($message);
|
||||
return true;
|
||||
}
|
||||
|
||||
private function tryMTProtoV1Decrypt($message_key, $chat_id, $old, $encrypted_data): string
|
||||
{
|
||||
[$aes_key, $aes_iv] = Crypt::oldKdf($message_key, $this->secret_chats[$chat_id][$old ? 'old_key' : 'key']['auth_key'], true);
|
||||
$decrypted_data = Crypt::igeDecrypt($encrypted_data, $aes_key, $aes_iv);
|
||||
$message_data_length = \unpack('V', \substr($decrypted_data, 0, 4))[1];
|
||||
$message_data = \substr($decrypted_data, 4, $message_data_length);
|
||||
if ($message_data_length > \strlen($decrypted_data)) {
|
||||
throw new SecurityException('message_data_length is too big');
|
||||
}
|
||||
if ($message_key != \substr(\sha1(\substr($decrypted_data, 0, 4 + $message_data_length), true), -16)) {
|
||||
throw new SecurityException('Msg_key mismatch');
|
||||
}
|
||||
if (\strlen($decrypted_data) - 4 - $message_data_length > 15) {
|
||||
throw new SecurityException('difference between message_data_length and the length of the remaining decrypted buffer is too big');
|
||||
}
|
||||
if (\strlen($decrypted_data) % 16 != 0) {
|
||||
throw new SecurityException("Length of decrypted data is not divisible by 16");
|
||||
}
|
||||
return $message_data;
|
||||
}
|
||||
|
||||
private function tryMTProtoV2Decrypt($message_key, $chat_id, $old, $encrypted_data): string
|
||||
{
|
||||
[$aes_key, $aes_iv] = Crypt::kdf($message_key, $this->secret_chats[$chat_id][$old ? 'old_key' : 'key']['auth_key'], !$this->secret_chats[$chat_id]['admin']);
|
||||
$decrypted_data = Crypt::igeDecrypt($encrypted_data, $aes_key, $aes_iv);
|
||||
if ($message_key != \substr(\hash('sha256', \substr($this->secret_chats[$chat_id][$old ? 'old_key' : 'key']['auth_key'], 88 + ($this->secret_chats[$chat_id]['admin'] ? 8 : 0), 32).$decrypted_data, true), 8, 16)) {
|
||||
throw new SecurityException('Msg_key mismatch');
|
||||
}
|
||||
$message_data_length = \unpack('V', \substr($decrypted_data, 0, 4))[1];
|
||||
$message_data = \substr($decrypted_data, 4, $message_data_length);
|
||||
if ($message_data_length > \strlen($decrypted_data)) {
|
||||
throw new SecurityException('message_data_length is too big');
|
||||
}
|
||||
if (\strlen($decrypted_data) - 4 - $message_data_length < 12) {
|
||||
throw new SecurityException('padding is too small');
|
||||
}
|
||||
if (\strlen($decrypted_data) - 4 - $message_data_length > 1024) {
|
||||
throw new SecurityException('padding is too big');
|
||||
}
|
||||
if (\strlen($decrypted_data) % 16 != 0) {
|
||||
throw new SecurityException("Length of decrypted data is not divisible by 16");
|
||||
}
|
||||
return $message_data;
|
||||
}
|
||||
}
|
30
src/SecretChats/RekeyState.php
Normal file
30
src/SecretChats/RekeyState.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Secret chat module.
|
||||
*
|
||||
* This file is part of MadelineProto.
|
||||
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU Affero General Public License for more details.
|
||||
* You should have received a copy of the GNU General Public License along with MadelineProto.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
|
||||
* @link https://docs.madelineproto.xyz MadelineProto documentation
|
||||
*/
|
||||
|
||||
namespace danog\MadelineProto\SecretChats;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
enum RekeyState {
|
||||
case IDLE;
|
||||
case REQUESTED;
|
||||
case ACCEPTED;
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* ResponseHandler module.
|
||||
*
|
||||
* This file is part of MadelineProto.
|
||||
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU Affero General Public License for more details.
|
||||
* You should have received a copy of the GNU General Public License along with MadelineProto.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
|
||||
* @link https://docs.madelineproto.xyz MadelineProto documentation
|
||||
*/
|
||||
|
||||
namespace danog\MadelineProto\SecretChats;
|
||||
|
||||
use danog\MadelineProto\Logger;
|
||||
use danog\MadelineProto\ResponseException;
|
||||
|
||||
/**
|
||||
* Manages responses.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait ResponseHandler
|
||||
{
|
||||
private function handleDecryptedUpdate(array $update): void
|
||||
{
|
||||
$chatId = $update['message']['chat_id'];
|
||||
$decryptedMessage = $update['message']['decrypted_message'];
|
||||
if ($decryptedMessage['_'] === 'decryptedMessage') {
|
||||
$this->saveUpdate($update);
|
||||
return;
|
||||
}
|
||||
if ($decryptedMessage['_'] === 'decryptedMessageService') {
|
||||
$action = $decryptedMessage['action'];
|
||||
switch ($action['_']) {
|
||||
case 'decryptedMessageActionRequestKey':
|
||||
$this->acceptRekey($chatId, $action);
|
||||
return;
|
||||
case 'decryptedMessageActionAcceptKey':
|
||||
$this->commitRekey($chatId, $action);
|
||||
return;
|
||||
case 'decryptedMessageActionCommitKey':
|
||||
$this->completeRekey($chatId, $action);
|
||||
return;
|
||||
case 'decryptedMessageActionNotifyLayer':
|
||||
$this->secret_chats[$chatId]['layer'] = $action['layer'];
|
||||
if ($action['layer'] >= 17 && \time() - $this->secret_chats[$chatId]['created'] > 15) {
|
||||
$this->notifyLayer($chatId);
|
||||
}
|
||||
if ($action['layer'] >= 73) {
|
||||
$this->secret_chats[$chatId]['mtproto'] = 2;
|
||||
}
|
||||
return;
|
||||
case 'decryptedMessageActionSetMessageTTL':
|
||||
$this->secret_chats[$chatId]['ttl'] = $action['ttl_seconds'];
|
||||
$this->saveUpdate($update);
|
||||
return;
|
||||
case 'decryptedMessageActionNoop':
|
||||
return;
|
||||
case 'decryptedMessageActionResend':
|
||||
$action['start_seq_no'] -= $this->secret_chats[$chatId]['out_seq_no_x'];
|
||||
$action['end_seq_no'] -= $this->secret_chats[$chatId]['out_seq_no_x'];
|
||||
$action['start_seq_no'] /= 2;
|
||||
$action['end_seq_no'] /= 2;
|
||||
$this->logger->logger('Resending messages for secret chat '.$chatId, Logger::WARNING);
|
||||
foreach ($this->secret_chats[$chatId]['outgoing'] as $seq => $message) {
|
||||
if ($seq >= $action['start_seq_no'] && $seq <= $action['end_seq_no']) {
|
||||
$this->methodCallAsyncRead('messages.sendEncrypted', ['peer' => $chatId, 'message' => $message]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
default:
|
||||
$this->saveUpdate($update);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ($decryptedMessage['_'] === 'decryptedMessageLayer') {
|
||||
if (($this->checkSecretOutSeqNo($chatId, $decryptedMessage['out_seq_no']))
|
||||
&& ($this->checkSecretInSeqNo($chatId, $decryptedMessage['in_seq_no']))) {
|
||||
$this->secret_chats[$chatId]['in_seq_no']++;
|
||||
if ($decryptedMessage['layer'] >= 17 && $decryptedMessage['layer'] !== $this->secret_chats[$chatId]['layer']) {
|
||||
$this->secret_chats[$chatId]['layer'] = $decryptedMessage['layer'];
|
||||
if ($decryptedMessage['layer'] >= 17 && \time() - $this->secret_chats[$chatId]['created'] > 15) {
|
||||
$this->notifyLayer($chatId);
|
||||
}
|
||||
}
|
||||
$update['message']['decrypted_message'] = $decryptedMessage['message'];
|
||||
$this->handleDecryptedUpdate($update);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new ResponseException('Unrecognized decrypted message received: '.\var_export($update, true));
|
||||
}
|
||||
}
|
48
src/SecretChats/SecretChat.php
Normal file
48
src/SecretChats/SecretChat.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Secret chat module.
|
||||
*
|
||||
* This file is part of MadelineProto.
|
||||
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU Affero General Public License for more details.
|
||||
* You should have received a copy of the GNU General Public License along with MadelineProto.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
|
||||
* @link https://docs.madelineproto.xyz MadelineProto documentation
|
||||
*/
|
||||
|
||||
namespace danog\MadelineProto\SecretChats;
|
||||
|
||||
use AssertionError;
|
||||
use danog\MadelineProto\Logger;
|
||||
use danog\MadelineProto\Loop\Secret\SecretFeedLoop;
|
||||
use danog\MadelineProto\MTProto;
|
||||
use danog\MadelineProto\MTProtoTools\Crypt;
|
||||
use danog\MadelineProto\SecurityException;
|
||||
use danog\MadelineProto\Tools;
|
||||
use phpseclib3\Math\BigInteger;
|
||||
|
||||
/**
|
||||
* Represents a secret chat.
|
||||
*/
|
||||
final class SecretChat {
|
||||
public function __construct(
|
||||
public readonly int $chatId,
|
||||
public readonly bool $creator,
|
||||
public readonly int $otherID,
|
||||
/** Creation date */
|
||||
public readonly int $created,
|
||||
public int $ttl = 0
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
521
src/SecretChats/SecretChatController.php
Normal file
521
src/SecretChats/SecretChatController.php
Normal file
@ -0,0 +1,521 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Secret chat module.
|
||||
*
|
||||
* This file is part of MadelineProto.
|
||||
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU Affero General Public License for more details.
|
||||
* You should have received a copy of the GNU General Public License along with MadelineProto.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
|
||||
* @link https://docs.madelineproto.xyz MadelineProto documentation
|
||||
*/
|
||||
|
||||
namespace danog\MadelineProto\SecretChats;
|
||||
|
||||
use Amp\Sync\LocalMutex;
|
||||
use AssertionError;
|
||||
use danog\MadelineProto\Logger;
|
||||
use danog\MadelineProto\Loop\Secret\SecretFeedLoop;
|
||||
use danog\MadelineProto\Loop\Update\UpdateLoop;
|
||||
use danog\MadelineProto\MTProto;
|
||||
use danog\MadelineProto\MTProtoTools\Crypt;
|
||||
use danog\MadelineProto\MTProtoTools\DialogId;
|
||||
use danog\MadelineProto\ResponseException;
|
||||
use danog\MadelineProto\RPCErrorException;
|
||||
use danog\MadelineProto\SecurityException;
|
||||
use danog\MadelineProto\Tools;
|
||||
use phpseclib3\Math\BigInteger;
|
||||
use Revolt\EventLoop;
|
||||
use SplQueue;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Represents a secret chat.
|
||||
* @internal
|
||||
*
|
||||
* @psalm-type TKey=array{auth_key: string, fingerprint: string, visualization_orig: string, visualization_46: string}
|
||||
*/
|
||||
final class SecretChatController implements Stringable {
|
||||
|
||||
/**
|
||||
* @var array<int, array>
|
||||
*/
|
||||
private array $incoming = [];
|
||||
/**
|
||||
* @var array<int, array>
|
||||
*/
|
||||
private array $outgoing = [];
|
||||
private int $in_seq_no = 0;
|
||||
private int $out_seq_no = 0;
|
||||
private int $layer = 8;
|
||||
private int $updated;
|
||||
/**
|
||||
* Secret queue.
|
||||
*
|
||||
* @var SplQueue<list{message, array}>
|
||||
*/
|
||||
private SplQueue $secretQueue;
|
||||
|
||||
private RekeyState $rekeyState = RekeyState::IDLE;
|
||||
private ?int $rekeyExchangeId = null;
|
||||
private ?BigInteger $rekeyParam = null;
|
||||
private ?array $rekeyKey;
|
||||
|
||||
/** @var TKey */
|
||||
private array $key;
|
||||
/** @var ?TKey */
|
||||
private ?array $oldKey;
|
||||
|
||||
private int $ttr = 100;
|
||||
|
||||
private int $mtproto = 1;
|
||||
|
||||
private readonly int $id;
|
||||
private SecretFeedLoop $feedLoop;
|
||||
public function __construct(
|
||||
private readonly MTProto $API,
|
||||
public readonly SecretChat $public,
|
||||
private int $in_seq_no_x,
|
||||
private int $out_seq_no_x
|
||||
)
|
||||
{
|
||||
$this->updated = $public->created;
|
||||
$this->id = DialogId::toSecretChatId($public->chatId);
|
||||
$this->feedLoop = new SecretFeedLoop($API, $this);
|
||||
$this->feedLoop->start();
|
||||
$this->secretQueue = new SplQueue;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Discard secret chat.
|
||||
*/
|
||||
public function discard(): void
|
||||
{
|
||||
$this->API->discardSecretChat($this->id);
|
||||
}
|
||||
public function notifyLayer(): void
|
||||
{
|
||||
$this->API->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $this->id, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionNotifyLayer', 'layer' => $this->API->getTL()->getSecretLayer()]]]);
|
||||
}
|
||||
private LocalMutex $rekeyMutex;
|
||||
/**
|
||||
* Rekey secret chat.
|
||||
*/
|
||||
public function rekey(): void
|
||||
{
|
||||
if ($this->rekeyState !== RekeyState::IDLE) {
|
||||
return;
|
||||
}
|
||||
$lock = $this->rekeyMutex->acquire();
|
||||
try {
|
||||
if ($this->rekeyState !== RekeyState::IDLE) {
|
||||
return;
|
||||
}
|
||||
$dh_config = $this->API->getDhConfig();
|
||||
$this->API->logger->logger('Rekeying secret chat '.$this.'...', Logger::VERBOSE);
|
||||
$this->API->logger->logger('Generating a...', Logger::VERBOSE);
|
||||
$a = new BigInteger(Tools::random(256), 256);
|
||||
$this->API->logger->logger('Generating g_a...', Logger::VERBOSE);
|
||||
$g_a = $dh_config['g']->powMod($a, $dh_config['p']);
|
||||
Crypt::checkG($g_a, $dh_config['p']);
|
||||
$this->rekeyState = RekeyState::REQUESTED;
|
||||
$this->rekeyExchangeId = Tools::randomInt();
|
||||
$this->rekeyParam = $a;
|
||||
$this->API->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $this->id, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionRequestKey', 'g_a' => $g_a->toBytes(), 'exchange_id' => $e]]]);
|
||||
$this->API->updaters[UpdateLoop::GENERIC]->resume();
|
||||
} finally {
|
||||
EventLoop::queue($lock->release(...));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Accept rekeying.
|
||||
*
|
||||
* @param array $params Parameters
|
||||
*/
|
||||
private function acceptRekey(array $params): void
|
||||
{
|
||||
$lock = $this->rekeyMutex->acquire();
|
||||
try {
|
||||
if ($this->rekeyState !== RekeyState::IDLE) {
|
||||
if ($this->rekeyExchangeId > $params['exchange_id']) {
|
||||
return;
|
||||
}
|
||||
if ($this->rekeyExchangeId === $params['exchange_id']) {
|
||||
$this->rekeyState = RekeyState::IDLE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->API->logger->logger('Accepting rekeying of '.$this.'...', Logger::VERBOSE);
|
||||
$dh_config = $this->API->getDhConfig();
|
||||
$this->API->logger->logger('Generating b...', Logger::VERBOSE);
|
||||
$b = new BigInteger(Tools::random(256), 256);
|
||||
$params['g_a'] = new BigInteger((string) $params['g_a'], 256);
|
||||
Crypt::checkG($params['g_a'], $dh_config['p']);
|
||||
$key = ['auth_key' => \str_pad($params['g_a']->powMod($b, $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT)];
|
||||
$key['fingerprint'] = \substr(\sha1($key['auth_key'], true), -8);
|
||||
$key['visualization_orig'] = $this->key['visualization_orig'];
|
||||
$key['visualization_46'] = \substr(\hash('sha256', $key['auth_key'], true), 20);
|
||||
|
||||
$this->rekeyState = RekeyState::ACCEPTED;
|
||||
$this->rekeyExchangeId = $params['exchange_id'];
|
||||
$this->rekeyKey = $key;
|
||||
|
||||
$g_b = $dh_config['g']->powMod($b, $dh_config['p']);
|
||||
Crypt::checkG($g_b, $dh_config['p']);
|
||||
$this->API->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $this->id, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionAcceptKey', 'g_b' => $g_b->toBytes(), 'exchange_id' => $params['exchange_id'], 'key_fingerprint' => $key['fingerprint']]]]);
|
||||
$this->API->updaters[UpdateLoop::GENERIC]->resume();
|
||||
} finally {
|
||||
EventLoop::queue($lock->release(...));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Commit rekeying of secret chat.
|
||||
*
|
||||
* @param array $params Parameters
|
||||
*/
|
||||
private function commitRekey(array $params): void
|
||||
{
|
||||
if ($this->rekeyState !== RekeyState::REQUESTED || $this->rekeyExchangeId !== $params['exchange_id']) {
|
||||
$this->rekeyState = RekeyState::IDLE;
|
||||
return;
|
||||
}
|
||||
$this->API->logger->logger('Committing rekeying of '.$this.'...', Logger::VERBOSE);
|
||||
$dh_config = ($this->API->getDhConfig());
|
||||
$params['g_b'] = new BigInteger((string) $params['g_b'], 256);
|
||||
Crypt::checkG($params['g_b'], $dh_config['p']);
|
||||
$key = ['auth_key' => \str_pad($params['g_b']->powMod($this->rekeyParam, $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT)];
|
||||
$key['fingerprint'] = \substr(\sha1($key['auth_key'], true), -8);
|
||||
$key['visualization_orig'] = $this->key['visualization_orig'];
|
||||
$key['visualization_46'] = \substr(\hash('sha256', $key['auth_key'], true), 20);
|
||||
if ($key['fingerprint'] !== $params['key_fingerprint']) {
|
||||
$this->API->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $this->id, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionAbortKey', 'exchange_id' => $params['exchange_id']]]]);
|
||||
throw new SecurityException('Invalid key fingerprint!');
|
||||
}
|
||||
$this->API->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $this->id, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionCommitKey', 'exchange_id' => $params['exchange_id'], 'key_fingerprint' => $key['fingerprint']]]]);
|
||||
$this->rekeyState = RekeyState::IDLE;
|
||||
$this->oldKey = $this->key;
|
||||
$this->key = $key;
|
||||
$this->ttr = 100;
|
||||
$this->updated = time();
|
||||
$this->API->updaters[UpdateLoop::GENERIC]->resume();
|
||||
}
|
||||
/**
|
||||
* Complete rekeying.
|
||||
*
|
||||
* @param array $params Parameters
|
||||
*/
|
||||
private function completeRekey(array $params): bool
|
||||
{
|
||||
if ($this->rekeyState !== RekeyState::ACCEPTED || $this->rekeyExchangeId !== $params['exchange_id']) {
|
||||
return false;
|
||||
}
|
||||
if ($this->rekeyKey['fingerprint'] !== $params['key_fingerprint']) {
|
||||
$this->API->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $this->id, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionAbortKey', 'exchange_id' => $params['exchange_id']]]]);
|
||||
throw new SecurityException('Invalid key fingerprint!');
|
||||
}
|
||||
$this->API->logger->logger('Completing rekeying of secret chat '.$this.'...', Logger::VERBOSE);
|
||||
$this->rekeyState = RekeyState::IDLE;
|
||||
$this->oldKey = $this->key;
|
||||
$this->key = $this->rekeyKey;
|
||||
$this->ttr = 100;
|
||||
$this->updated = time();
|
||||
$this->API->methodCallAsyncRead('messages.sendEncryptedService', ['peer' => $this->id, 'message' => ['_' => 'decryptedMessageService', 'action' => ['_' => 'decryptedMessageActionNoop']]]);
|
||||
$this->API->logger->logger('Secret chat '.$this.' rekeyed successfully!', Logger::VERBOSE);
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Encrypt secret chat message.
|
||||
* @internal
|
||||
*/
|
||||
public function encryptSecretMessage(MTProtoOutgoingMessage $msg): string
|
||||
{
|
||||
$body = $msg->getBody();
|
||||
if (isset($body['data'])) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$this->ttr--;
|
||||
if ($this->layer > 8) {
|
||||
if (($this->ttr <= 0 || \time() - $this->updated > 7 * 24 * 60 * 60) && $this->rekeyState === RekeyState::IDLE) {
|
||||
$this->rekey();
|
||||
}
|
||||
}
|
||||
|
||||
$body['data'] = $this->encryptSecretMessageInner($body['message']);
|
||||
unset($body['message']);
|
||||
$this->secretQueue->enqueue([$msg->getConstructor(), $body]);
|
||||
|
||||
return $body;
|
||||
}
|
||||
private function encryptSecretMessageInner(array $message): void {
|
||||
$message['random_id'] = Tools::random(8);
|
||||
if ($this->layer > 8) {
|
||||
$message = ['_' => 'decryptedMessageLayer', 'layer' => $this->layer, 'in_seq_no' => $this->generateSecretInSeqNo(), 'out_seq_no' => $this->generateSecretOutSeqNo(), 'message' => $message];
|
||||
$this->out_seq_no++;
|
||||
}
|
||||
$this->outgoing[$this->out_seq_no] = $message;
|
||||
$constructor = $this->layer === 8 ? 'DecryptedMessage' : 'DecryptedMessageLayer';
|
||||
$message = $this->API->getTL()->serializeObject(['type' => $constructor], $message, $constructor, $this->layer);
|
||||
$message = Tools::packUnsignedInt(\strlen($message)).$message;
|
||||
if ($this->mtproto === 2) {
|
||||
$padding = Tools::posmod(-\strlen($message), 16);
|
||||
if ($padding < 12) {
|
||||
$padding += 16;
|
||||
}
|
||||
$message .= Tools::random($padding);
|
||||
$message_key = \substr(\hash('sha256', \substr($this->key['auth_key'], 88 + ($this->public->creator ? 0 : 8), 32).$message, true), 8, 16);
|
||||
[$aes_key, $aes_iv] = Crypt::kdf($message_key, $this->key['auth_key'], $this->public->creator);
|
||||
} else {
|
||||
$message_key = \substr(\sha1($message, true), -16);
|
||||
[$aes_key, $aes_iv] = Crypt::oldKdf($message_key, $this->key['auth_key'], true);
|
||||
$message .= Tools::random(Tools::posmod(-\strlen($message), 16));
|
||||
}
|
||||
$message = $this->key['fingerprint'].$message_key.Crypt::igeEncrypt($message, $aes_key, $aes_iv);
|
||||
}
|
||||
|
||||
private function handleDecryptedUpdate(array $update): void
|
||||
{
|
||||
$decryptedMessage = $update['message']['decrypted_message'];
|
||||
if ($decryptedMessage['_'] === 'decryptedMessage') {
|
||||
$this->API->saveUpdate($update);
|
||||
return;
|
||||
}
|
||||
if ($decryptedMessage['_'] === 'decryptedMessageService') {
|
||||
$action = $decryptedMessage['action'];
|
||||
switch ($action['_']) {
|
||||
case 'decryptedMessageActionRequestKey':
|
||||
$this->acceptRekey($action);
|
||||
return;
|
||||
case 'decryptedMessageActionAcceptKey':
|
||||
$this->commitRekey($action);
|
||||
return;
|
||||
case 'decryptedMessageActionCommitKey':
|
||||
$this->completeRekey($action);
|
||||
return;
|
||||
case 'decryptedMessageActionNotifyLayer':
|
||||
$this->layer = $action['layer'];
|
||||
if ($action['layer'] >= 17 && \time() - $this->public->created > 15) {
|
||||
$this->notifyLayer();
|
||||
}
|
||||
if ($action['layer'] >= 73) {
|
||||
$this->mtproto = 2;
|
||||
}
|
||||
return;
|
||||
case 'decryptedMessageActionSetMessageTTL':
|
||||
$this->public->ttl = $action['ttl_seconds'];
|
||||
$this->API->saveUpdate($update);
|
||||
return;
|
||||
case 'decryptedMessageActionNoop':
|
||||
return;
|
||||
case 'decryptedMessageActionResend':
|
||||
$action['start_seq_no'] -= $this->out_seq_no_x;
|
||||
$action['end_seq_no'] -= $this->out_seq_no_x;
|
||||
$action['start_seq_no'] /= 2;
|
||||
$action['end_seq_no'] /= 2;
|
||||
$this->API->logger->logger('Resending messages for '.$this, Logger::WARNING);
|
||||
foreach ($this->outgoing as $seq => $message) {
|
||||
if ($seq >= $action['start_seq_no'] && $seq <= $action['end_seq_no']) {
|
||||
$this->API->methodCallAsyncRead('messages.sendEncrypted', ['peer' => $this->id, 'message' => $message]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
default:
|
||||
$this->API->saveUpdate($update);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ($decryptedMessage['_'] === 'decryptedMessageLayer') {
|
||||
if (($this->checkSecretOutSeqNo($decryptedMessage['out_seq_no']))
|
||||
&& ($this->checkSecretInSeqNo($decryptedMessage['in_seq_no']))) {
|
||||
$this->in_seq_no++;
|
||||
if ($decryptedMessage['layer'] >= 17 && $decryptedMessage['layer'] !== $this->layer) {
|
||||
$this->layer = $decryptedMessage['layer'];
|
||||
if ($decryptedMessage['layer'] >= 17 && \time() - $this->public->created > 15) {
|
||||
$this->notifyLayer();
|
||||
}
|
||||
}
|
||||
$update['message']['decrypted_message'] = $decryptedMessage['message'];
|
||||
$this->handleDecryptedUpdate($update);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new ResponseException('Unrecognized decrypted message received: '.\var_export($update, true));
|
||||
}
|
||||
/**
|
||||
* Handle encrypted update.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function handleEncryptedUpdate(array $message): bool
|
||||
{
|
||||
$message['message']['bytes'] = (string) $message['message']['bytes'];
|
||||
$auth_key_id = \substr($message['message']['bytes'], 0, 8);
|
||||
$old = false;
|
||||
if ($auth_key_id !== $this->key['fingerprint']) {
|
||||
if (isset($this->oldKey['fingerprint'])) {
|
||||
if ($auth_key_id !== $this->oldKey['fingerprint']) {
|
||||
$this->discard();
|
||||
throw new SecurityException('Key fingerprint mismatch');
|
||||
}
|
||||
$old = true;
|
||||
} else {
|
||||
$this->discard();
|
||||
throw new SecurityException('Key fingerprint mismatch');
|
||||
}
|
||||
}
|
||||
$message_key = \substr($message['message']['bytes'], 8, 16);
|
||||
$encrypted_data = \substr($message['message']['bytes'], 24);
|
||||
if ($this->mtproto === 2) {
|
||||
$this->API->logger->logger('Trying MTProto v2 decryption for '.$this.'...', Logger::NOTICE);
|
||||
try {
|
||||
$message_data = $this->tryMTProtoV2Decrypt($message_key, $old, $encrypted_data);
|
||||
$this->API->logger->logger('MTProto v2 decryption OK for '.$this.'...', Logger::NOTICE);
|
||||
} catch (SecurityException $e) {
|
||||
$this->API->logger->logger('MTProto v2 decryption failed with message '.$e->getMessage().', trying MTProto v1 decryption for '.$this.'...', Logger::NOTICE);
|
||||
$message_data = $this->tryMTProtoV1Decrypt($message_key, $old, $encrypted_data);
|
||||
$this->API->logger->logger('MTProto v1 decryption OK for '.$this.'...', Logger::NOTICE);
|
||||
$this->mtproto = 1;
|
||||
}
|
||||
} else {
|
||||
$this->API->logger->logger('Trying MTProto v1 decryption for '.$this.'...', Logger::NOTICE);
|
||||
try {
|
||||
$message_data = $this->tryMTProtoV1Decrypt($message_key, $old, $encrypted_data);
|
||||
$this->API->logger->logger('MTProto v1 decryption OK for '.$this.'...', Logger::NOTICE);
|
||||
} catch (SecurityException $e) {
|
||||
$this->API->logger->logger('MTProto v1 decryption failed with message '.$e->getMessage().', trying MTProto v2 decryption for '.$this.'...', Logger::NOTICE);
|
||||
$message_data = $this->tryMTProtoV2Decrypt($message_key, $old, $encrypted_data);
|
||||
$this->API->logger->logger('MTProto v2 decryption OK for '.$this.'...', Logger::NOTICE);
|
||||
$this->mtproto = 2;
|
||||
}
|
||||
}
|
||||
$deserialized = $this->API->getTL()->deserialize($message_data, ['type' => '']);
|
||||
$this->ttr--;
|
||||
if (($this->ttr <= 0 || \time() - $this->updated > 7 * 24 * 60 * 60) && $this->rekeyState === RekeyState::IDLE) {
|
||||
$this->rekey($message['message']['chat_id']);
|
||||
}
|
||||
unset($message['message']['bytes']);
|
||||
$message['message']['decrypted_message'] = $deserialized;
|
||||
$this->incoming[$this->in_seq_no] = $message['message'];
|
||||
$this->handleDecryptedUpdate($message);
|
||||
return true;
|
||||
}
|
||||
|
||||
private function tryMTProtoV1Decrypt(string $message_key, bool $old, string $encrypted_data): string
|
||||
{
|
||||
[$aes_key, $aes_iv] = Crypt::oldKdf($message_key, ($old ? $this->oldKey : $this->key)['auth_key'], true);
|
||||
$decrypted_data = Crypt::igeDecrypt($encrypted_data, $aes_key, $aes_iv);
|
||||
$message_data_length = \unpack('V', \substr($decrypted_data, 0, 4))[1];
|
||||
$message_data = \substr($decrypted_data, 4, $message_data_length);
|
||||
if ($message_data_length > \strlen($decrypted_data)) {
|
||||
throw new SecurityException('message_data_length is too big');
|
||||
}
|
||||
if ($message_key != \substr(\sha1(\substr($decrypted_data, 0, 4 + $message_data_length), true), -16)) {
|
||||
throw new SecurityException('Msg_key mismatch');
|
||||
}
|
||||
if (\strlen($decrypted_data) - 4 - $message_data_length > 15) {
|
||||
throw new SecurityException('difference between message_data_length and the length of the remaining decrypted buffer is too big');
|
||||
}
|
||||
if (\strlen($decrypted_data) % 16 != 0) {
|
||||
throw new SecurityException("Length of decrypted data is not divisible by 16");
|
||||
}
|
||||
return $message_data;
|
||||
}
|
||||
|
||||
private function tryMTProtoV2Decrypt(string $message_key, bool $old, string $encrypted_data): string
|
||||
{
|
||||
$key = ($old ? $this->oldKey : $this->key)['auth_key'];
|
||||
[$aes_key, $aes_iv] = Crypt::kdf($message_key, $key, !$this->public->creator);
|
||||
$decrypted_data = Crypt::igeDecrypt($encrypted_data, $aes_key, $aes_iv);
|
||||
if ($message_key != \substr(\hash('sha256', \substr($key, 88 + ($this->public->creator ? 8 : 0), 32).$decrypted_data, true), 8, 16)) {
|
||||
throw new SecurityException('Msg_key mismatch');
|
||||
}
|
||||
$message_data_length = \unpack('V', \substr($decrypted_data, 0, 4))[1];
|
||||
$message_data = \substr($decrypted_data, 4, $message_data_length);
|
||||
if ($message_data_length > \strlen($decrypted_data)) {
|
||||
throw new SecurityException('message_data_length is too big');
|
||||
}
|
||||
if (\strlen($decrypted_data) - 4 - $message_data_length < 12) {
|
||||
throw new SecurityException('padding is too small');
|
||||
}
|
||||
if (\strlen($decrypted_data) - 4 - $message_data_length > 1024) {
|
||||
throw new SecurityException('padding is too big');
|
||||
}
|
||||
if (\strlen($decrypted_data) % 16 != 0) {
|
||||
throw new SecurityException("Length of decrypted data is not divisible by 16");
|
||||
}
|
||||
return $message_data;
|
||||
}
|
||||
|
||||
private function checkSecretInSeqNo(int $seqno): bool
|
||||
{
|
||||
$seqno = ($seqno - $this->out_seq_no_x) / 2;
|
||||
$last = 0;
|
||||
foreach ($this->incoming as $message) {
|
||||
if (isset($message['decrypted_message']['in_seq_no'])) {
|
||||
if (($message['decrypted_message']['in_seq_no'] - $this->out_seq_no_x) / 2 < $last) {
|
||||
$this->API->logger->logger("Discarding $this, in_seq_no is not increasing", Logger::LEVEL_FATAL);
|
||||
$this->discard();
|
||||
throw new SecurityException('in_seq_no is not increasing');
|
||||
}
|
||||
$last = ($message['decrypted_message']['in_seq_no'] - $this->out_seq_no_x) / 2;
|
||||
}
|
||||
}
|
||||
if ($seqno > $this->out_seq_no + 1) {
|
||||
$this->API->logger->logger("Discarding $this, in_seq_no is too big", Logger::LEVEL_FATAL);
|
||||
$this->discard();
|
||||
throw new SecurityException('in_seq_no is too big');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private function checkSecretOutSeqNo(int $seqno): bool
|
||||
{
|
||||
$seqno = ($seqno - $this->in_seq_no_x) / 2;
|
||||
$C = 0;
|
||||
foreach ($this->incoming as $message) {
|
||||
if (isset($message['decrypted_message']['out_seq_no']) && $C < $this->in_seq_no) {
|
||||
$temp = ($message['decrypted_message']['out_seq_no'] - $this->in_seq_no_x) / 2;
|
||||
if ($temp !== $C) {
|
||||
$this->API->logger->logger("Discarding $this, out_seq_no hole: should be $C, is $temp", Logger::LEVEL_FATAL);
|
||||
$this->discard();
|
||||
throw new SecurityException("out_seq_no hole: should be $C, is $temp");
|
||||
}
|
||||
$C++;
|
||||
}
|
||||
}
|
||||
//$this->API->logger->logger($C, $seqno);
|
||||
if ($seqno < $C) {
|
||||
// <= C
|
||||
$this->API->logger->logger('WARNING: dropping repeated message with seqno '.$seqno);
|
||||
return false;
|
||||
}
|
||||
if ($seqno > $C) {
|
||||
// > C+1
|
||||
$this->API->logger->logger("Discarding $this, out_seq_no gap detected: ($seqno > $C)", Logger::LEVEL_FATAL);
|
||||
$this->discard();
|
||||
throw new SecurityException('WARNING: out_seq_no gap detected ('.$seqno.' > '.$C.')!');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private function generateSecretInSeqNo(): int
|
||||
{
|
||||
return $this->layer > 8 ? $this->in_seq_no * 2 + $this->in_seq_no_x : -1;
|
||||
}
|
||||
private function generateSecretOutSeqNo(): int
|
||||
{
|
||||
return $this->layer > 8 ? $this->out_seq_no * 2 + $this->out_seq_no_x : -1;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return "secret chat {$this->id}";
|
||||
}
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SeqNoHandler module.
|
||||
*
|
||||
* This file is part of MadelineProto.
|
||||
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU Affero General Public License for more details.
|
||||
* You should have received a copy of the GNU General Public License along with MadelineProto.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
|
||||
* @link https://docs.madelineproto.xyz MadelineProto documentation
|
||||
*/
|
||||
|
||||
namespace danog\MadelineProto\SecretChats;
|
||||
|
||||
use danog\MadelineProto\Logger;
|
||||
use danog\MadelineProto\SecurityException;
|
||||
|
||||
/**
|
||||
* Manages sequence numbers.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait SeqNoHandler
|
||||
{
|
||||
private function checkSecretInSeqNo($chat_id, $seqno)
|
||||
{
|
||||
$seqno = ($seqno - $this->secret_chats[$chat_id]['out_seq_no_x']) / 2;
|
||||
$last = 0;
|
||||
foreach ($this->secret_chats[$chat_id]['incoming'] as $message) {
|
||||
if (isset($message['decrypted_message']['in_seq_no'])) {
|
||||
if (($message['decrypted_message']['in_seq_no'] - $this->secret_chats[$chat_id]['out_seq_no_x']) / 2 < $last) {
|
||||
$this->logger->logger("Discarding secret chat $chat_id, in_seq_no is not increasing", Logger::LEVEL_FATAL);
|
||||
$this->discardSecretChat($chat_id);
|
||||
throw new SecurityException('in_seq_no is not increasing');
|
||||
}
|
||||
$last = ($message['decrypted_message']['in_seq_no'] - $this->secret_chats[$chat_id]['out_seq_no_x']) / 2;
|
||||
}
|
||||
}
|
||||
if ($seqno > $this->secret_chats[$chat_id]['out_seq_no'] + 1) {
|
||||
$this->logger->logger("Discarding secret chat $chat_id, in_seq_no is too big", Logger::LEVEL_FATAL);
|
||||
$this->discardSecretChat($chat_id);
|
||||
throw new SecurityException('in_seq_no is too big');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private function checkSecretOutSeqNo($chat_id, $seqno)
|
||||
{
|
||||
$seqno = ($seqno - $this->secret_chats[$chat_id]['in_seq_no_x']) / 2;
|
||||
$C = 0;
|
||||
foreach ($this->secret_chats[$chat_id]['incoming'] as $message) {
|
||||
if (isset($message['decrypted_message']['out_seq_no']) && $C < $this->secret_chats[$chat_id]['in_seq_no']) {
|
||||
$temp = ($message['decrypted_message']['out_seq_no'] - $this->secret_chats[$chat_id]['in_seq_no_x']) / 2;
|
||||
if ($temp !== $C) {
|
||||
$this->logger->logger("Discarding secret chat $chat_id, out_seq_no hole: should be $C, is $temp", Logger::LEVEL_FATAL);
|
||||
$this->discardSecretChat($chat_id);
|
||||
throw new SecurityException("out_seq_no hole: should be $C, is $temp");
|
||||
}
|
||||
$C++;
|
||||
}
|
||||
}
|
||||
//$this->logger->logger($C, $seqno);
|
||||
if ($seqno < $C) {
|
||||
// <= C
|
||||
$this->logger->logger('WARNING: dropping repeated message with seqno '.$seqno);
|
||||
return false;
|
||||
}
|
||||
if ($seqno > $C) {
|
||||
// > C+1
|
||||
$this->logger->logger("Discarding secret chat $chat_id, out_seq_no gap detected: ($seqno > $C)", Logger::LEVEL_FATAL);
|
||||
$this->discardSecretChat($chat_id);
|
||||
throw new SecurityException('WARNING: out_seq_no gap detected ('.$seqno.' > '.$C.')!');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private function generateSecretInSeqNo($chat)
|
||||
{
|
||||
return $this->secret_chats[$chat]['layer'] > 8 ? $this->secret_chats[$chat]['in_seq_no'] * 2 + $this->secret_chats[$chat]['in_seq_no_x'] : -1;
|
||||
}
|
||||
private function generateSecretOutSeqNo($chat)
|
||||
{
|
||||
return $this->secret_chats[$chat]['layer'] > 8 ? $this->secret_chats[$chat]['out_seq_no'] * 2 + $this->secret_chats[$chat]['out_seq_no_x'] : -1;
|
||||
}
|
||||
}
|
@ -37,11 +37,6 @@ final class RPC extends SettingsAbstract
|
||||
*/
|
||||
protected int $floodTimeout = 30;
|
||||
|
||||
/**
|
||||
* Maximum number of message IDs to consider when using call queues.
|
||||
*/
|
||||
protected int $limitCallQueue = 100;
|
||||
|
||||
/**
|
||||
* Encode payload with GZIP if bigger than.
|
||||
*/
|
||||
@ -109,26 +104,6 @@ final class RPC extends SettingsAbstract
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum number of messages to consider when using call queues.
|
||||
*/
|
||||
public function getLimitCallQueue(): int
|
||||
{
|
||||
return $this->limitCallQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maximum number of messages to consider when using call queues.
|
||||
*
|
||||
* @param int $limitCallQueue Maximum number of messages to consider when using call queues
|
||||
*/
|
||||
public function setLimitCallQueue(int $limitCallQueue): self
|
||||
{
|
||||
$this->limitCallQueue = $limitCallQueue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encode payload with GZIP if bigger than.
|
||||
*/
|
||||
|
@ -666,10 +666,6 @@ final class TL implements TLInterface
|
||||
$serialized .= $this->serializeObject(['type' => 'bytes'], Tools::random(15 + 4 * Tools::randomInt(modulus: 3)), 'random_bytes');
|
||||
continue;
|
||||
}
|
||||
if ($current_argument['name'] === 'data' && isset($tl['method']) && \in_array($tl['method'], ['messages.sendEncrypted', 'messages.sendEncryptedFile', 'messages.sendEncryptedService'], true) && isset($arguments['message'])) {
|
||||
$serialized .= $this->serializeObject($current_argument, $this->API->encryptSecretMessage($arguments['peer']['chat_id'], $arguments['message'], $arguments['queuePromise']), 'data');
|
||||
continue;
|
||||
}
|
||||
if ($current_argument['name'] === 'random_id') {
|
||||
switch ($current_argument['type']) {
|
||||
case 'long':
|
||||
|
@ -42,7 +42,7 @@ trait TLParams
|
||||
$param['type'] = ($mtproto && $param['type'] === 'Message' ? 'MT' : '').$param['type'];
|
||||
$param['type'] = $mtproto && $param['type'] === '%Message' ? '%MTMessage' : $param['type'];
|
||||
|
||||
if (\in_array($param['name'], ['key_fingerprint', 'server_salt', 'new_server_salt', 'ping_id', 'exchange_id'], true) && $param['type'] === 'long') {
|
||||
if (\in_array($param['name'], ['key_fingerprint', 'server_salt', 'new_server_salt', 'ping_id'], true) && $param['type'] === 'long') {
|
||||
$param['type'] = 'strlong';
|
||||
} elseif (\in_array($param['name'], ['peer_tag', 'file_token', 'cdn_key', 'cdn_iv', 'encryption_key', 'encryption_iv'], true)) {
|
||||
$param['type'] = 'string';
|
||||
|
Loading…
Reference in New Issue
Block a user