MadelineProto/mtproto.php

340 lines
16 KiB
PHP
Raw Normal View History

2016-06-23 23:51:08 +02:00
<?php
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . DIRECTORY_SEPARATOR . 'libpy2php');
require_once ('libpy2php.php');
require_once ('os_path.php');
require_once ('crypt.php');
require_once ('prime.php');
require_once ('TL.php');
2016-07-13 21:39:24 +02:00
require_once ('Struct.php');
/**
* Function to get hex crc32
* :param data: Data to encode
*/
2016-06-23 23:51:08 +02:00
function newcrc32($data) {
2016-06-28 03:10:15 +02:00
return hexdec(hash("crc32b", $data));
2016-06-23 23:51:08 +02:00
}
2016-07-13 21:39:24 +02:00
2016-06-23 23:51:08 +02:00
/**
2016-07-13 21:39:24 +02:00
* Function to dump the hex version of a string.
* :param what: What to dump
2016-06-23 23:51:08 +02:00
*/
2016-06-28 03:40:31 +02:00
function hex_dump($what) { var_dump(bin2hex($what)); };
2016-07-13 21:39:24 +02:00
/**
* Function slice a certain string. Works just like array_slice, only with a string.
* :param data: data to slice
* :param offset: If offset is non-negative, the sequence will start at that offset in the string. If offset is negative, the sequence will start that far from the end of the string.
* :param length: If length is given and is positive, then the sequence will have up to that many chars in it. If the string is shorter than the length, then only the available string chars will be present. If length is given and is negative then the sequence will stop that many chars from the end of the string. If it is omitted, then the sequence will have everything from offset up until the end of the string.
*/
2016-06-28 13:38:39 +02:00
function file_slice($data, $offset, $length) {
$size = strlen($data);
if($offset < 0) $offset = $size + $offset;
if($length < 0) $length = $size + $length;
if($length == null) $length = $size;
if($offset == null) $offset = 0;
$packet_sliced = "";
while ($length != 0) { if($offset == $size) break; $packet_sliced .= $data[$offset]; $offset++; $length--; }
return $packet_sliced;
}
2016-07-13 21:39:24 +02:00
/**
* len.
*
* Get the length of a string or of an array
*
* @param $input String or array to parse
*
* @return int with the length
**/
function len($input)
{
if (is_array($input)) {
return count($input);
}
return strlen($input);
2016-06-28 03:10:15 +02:00
}
2016-07-13 21:39:24 +02:00
/**
* Function to visualize byte streams. Split into bytes, print to console.
* :param bs: BYTE STRING
*/
2016-06-23 23:51:08 +02:00
function vis($bs) {
2016-07-13 21:39:24 +02:00
$bs = str_split($bs);
2016-06-23 23:51:08 +02:00
$symbols_in_one_line = 8;
2016-07-13 21:39:24 +02:00
$n = floor(len($bs) / $symbols_in_one_line);
2016-06-23 23:51:08 +02:00
$i = 0;
foreach (pyjslib_range($n) as $i) {
2016-07-14 12:43:38 +02:00
echo $i * $symbols_in_one_line . ' | ' . join(' ',
array_map(function($el) { return bin2hex($el); }, array_slice($bs,$i*$symbols_in_one_line, ($i+1)*$symbols_in_one_line))
2016-07-13 21:39:24 +02:00
) . PHP_EOL;
2016-06-23 23:51:08 +02:00
}
2016-07-14 12:43:38 +02:00
if (!(len($bs) % $symbols_in_one_line == 0)) {
echo ($i + 1) * $symbols_in_one_line . ' | ' . join(' ',
array_map(function($el) { return bin2hex($el); }, array_slice($bs, ($i+1)*$symbols_in_one_line))
2016-07-13 21:39:24 +02:00
) . PHP_EOL;
2016-06-23 23:51:08 +02:00
}
}
2016-06-26 15:28:39 +02:00
/**
* bytes_to_long(string) : long
* Convert a byte string to a long integer.
* This is (essentially) the inverse of long_to_bytes().
*/
function bytes_to_long($s) {
$acc = 0;
2016-06-28 03:10:15 +02:00
$length = strlen($s);
2016-06-26 15:28:39 +02:00
if (($length % 4)) {
$extra = (4 - ($length % 4));
$s = (($b('') * $extra) + $s);
$length = ($length + $extra);
}
foreach( pyjslib_range(0, $length, 4) as $i ) {
2016-07-14 12:43:38 +02:00
$acc = ($acc << 32 + $this->struct->unpack('I', array_slice($s, $i, ($i + 4) - $i))[0]);
2016-06-26 15:28:39 +02:00
}
return $acc;
}
2016-06-24 23:47:10 +02:00
2016-06-27 16:52:28 +02:00
function fread_all($handle) {
$pos = ftell($handle);
fseek($handle, 0);
$content = fread($handle, fstat($handle)["size"]);
fseek($handle, $pos);
return $content;
}
2016-06-28 03:37:04 +02:00
function fopen_and_write($filename, $mode, $data) {
$handle = fopen($filename, $mode);
fwrite($handle, $data);
rewind($handle);
return $handle;
}
2016-06-26 15:28:39 +02:00
/**
* long_to_bytes(n:long, blocksize:int) : string
* Convert a long integer to a byte string.
* If optional blocksize is given and greater than zero, pad the front of the
* byte string with binary zeros so that the length is a multiple of
* blocksize.
*/
function long_to_bytes($n,$blocksize=0) {
$s = $b('');
$n = long($n);
while (($n > 0)) {
2016-07-14 12:43:38 +02:00
$s = ($this->struct->pack('I', $n & 4294967295) + $s);
2016-06-26 15:28:39 +02:00
$n = $n >> 32;
}
2016-06-28 03:10:15 +02:00
foreach( pyjslib_range(strlen($s)) as $i ) {
2016-06-26 15:28:39 +02:00
if (($s[$i] != $b('')[0])) {
break;
}
}
$s = array_slice($s, $i, null);
2016-06-28 03:10:15 +02:00
if (($blocksize > 0) && (strlen($s) % $blocksize)) {
$s = ((($blocksize - (strlen($s) % $blocksize)) * $b('')) + $s);
2016-06-26 15:28:39 +02:00
}
return $s;
}
2016-07-14 12:43:38 +02:00
function string2bin($string) {
$res = null;
foreach(explode('\\', $string) as $s) {
if($s != null && $s[0] == "x") {
$res .= hex2bin(substr($s, 1));
}
}
return $res;
}
2016-06-23 23:51:08 +02:00
/**
* Manages TCP Transport. encryption and message frames
*/
class Session {
function __construct($ip, $port, $auth_key = null, $server_salt = null) {
2016-06-27 22:52:07 +02:00
$this->sock = fsockopen("tcp://".$ip.":".$port);
2016-06-23 23:51:08 +02:00
$this->number = 0;
$this->timedelta = 0;
2016-06-26 14:53:16 +02:00
$this->session_id = random_bytes(8);
2016-06-23 23:51:08 +02:00
$this->auth_key = $auth_key;
2016-07-14 12:43:38 +02:00
$this->auth_key_id = $this->auth_key ? substr(sha1($this->auth_key, true), -8, null) : null;
2016-06-27 22:52:07 +02:00
stream_set_timeout($this->sock, 5);
2016-06-23 23:51:08 +02:00
$this->MAX_RETRY = 5;
$this->AUTH_MAX_RETRY = 5;
2016-07-14 12:43:38 +02:00
$this->struct = new \danog\PHP\Struct();
2016-06-23 23:51:08 +02:00
}
2016-06-27 22:52:07 +02:00
function __destruct() {
2016-06-26 14:53:16 +02:00
fclose($this->sock);
2016-06-23 23:51:08 +02:00
}
/**
* Forming the message frame and sending message to server
* :param message: byte string to send
*/
function send_message($message_data) {
2016-07-14 12:43:38 +02:00
$message_id = $this->struct->pack('<Q', (int)((time() + $this->timedelta) * pow(2, 30)) * 4);
2016-06-28 03:10:15 +02:00
2016-06-23 23:51:08 +02:00
if (($this->auth_key == null) || ($this->server_salt == null)) {
2016-07-14 12:43:38 +02:00
$message = string2bin('\x00\x00\x00\x00\x00\x00\x00\x00') . $message_id . $this->struct->pack('<I', strlen($message_data)) . $message_data;
2016-06-23 23:51:08 +02:00
} else {
2016-07-14 12:43:38 +02:00
$encrypted_data =
$this->server_salt . $this->session_id . $message_id . $this->struct->pack('<II', $this->number, strlen($message_data)) . $message_data;
$message_key = substr(sha1($encrypted_data, true), -16, null);
2016-06-28 03:10:15 +02:00
$padding = random_bytes((-strlen($encrypted_data) % 16));
2016-07-14 12:43:38 +02:00
echo strlen($encrypted_data . $padding) . PHP_EOL;
2016-06-23 23:51:08 +02:00
list($aes_key, $aes_iv) = $this->aes_calculate($message_key);
2016-07-14 12:43:38 +02:00
$message = $this->auth_key_id . $message_key . crypt::ige_encrypt($encrypted_data . $padding, $aes_key, $aes_iv);
2016-06-23 23:51:08 +02:00
}
2016-07-14 12:43:38 +02:00
$step1 = $this->struct->pack('<II', (strlen($message) + 12), $this->number) . $message;
$step2 = $step1 . $this->struct->pack('<I', newcrc32($step1));
2016-06-26 14:53:16 +02:00
fwrite($this->sock, $step2);
2016-07-14 12:43:38 +02:00
$this->number += 1;
2016-06-23 23:51:08 +02:00
}
/**
* Reading socket and receiving message from server. Check the CRC32.
*/
function recv_message() {
2016-06-26 14:53:16 +02:00
$packet_length_data = fread($this->sock, 4);
2016-06-28 03:10:15 +02:00
if (strlen($packet_length_data) < 4) {
2016-06-27 18:53:15 +02:00
throw new Exception('Nothing in the socket!');
2016-06-23 23:51:08 +02:00
}
2016-07-14 12:43:38 +02:00
$packet_length = $this->struct->pack('<I', $packet_length_data)[1];
2016-06-26 14:53:16 +02:00
$packet = fread($this->sock, ($packet_length - 4));
2016-06-28 13:38:39 +02:00
if (!(newcrc32($packet_length_data . file_slice($packet, 0, -4 - 0)) == unpack_le('I', file_slice($packet, -4, null))[1])) {
2016-06-27 18:53:15 +02:00
throw new Exception('CRC32 was not correct!');
2016-06-23 23:51:08 +02:00
}
2016-06-28 13:38:39 +02:00
$x = [unpack('I', file_slice($packet, null, 4))[1]];
2016-06-28 03:37:04 +02:00
$auth_key_id = file_slice($packet, 4, 12 - 4);
2016-06-23 23:51:08 +02:00
if (($auth_key_id == '')) {
2016-06-28 16:28:25 +02:00
$unpacked = unpack('c8/Iint', file_slice($packet, 12, 24 - 12));
$message_length = $unpacked["int"];
unset($unpacked["int"]);
2016-06-28 17:21:10 +02:00
$message_id = "";
foreach ($unpacked as $chr) {
$message_id .= chr($chr);
}
2016-06-28 13:38:39 +02:00
$data = file_slice($packet, 24, (24 + $message_length) - 24);
2016-06-23 23:51:08 +02:00
} else if (($auth_key_id == $this->auth_key_id)) {
$message_key = array_slice($packet, 12, 28 - 12);
$encrypted_data = array_slice($packet, 28, -4 - 28);
list($aes_key, $aes_iv) = py2php_kwargs_method_call($this, 'aes_calculate', [$message_key], ["direction" => 'from server']);
$decrypted_data = crypt::ige_decrypt($encrypted_data, $aes_key, $aes_iv);
assert((array_slice($decrypted_data, 0, 8 - 0) == $this->server_salt));
assert((array_slice($decrypted_data, 8, 16 - 8) == $this->session_id));
$message_id = array_slice($decrypted_data, 16, 24 - 16);
2016-06-27 16:52:28 +02:00
$seq_no = unpack('I', array_slice($decrypted_data, 24, 28 - 24)) [0];
$message_data_length = unpack('I', array_slice($decrypted_data, 28, 32 - 28)) [0];
2016-06-23 23:51:08 +02:00
$data = array_slice($decrypted_data, 32, (32 + $message_data_length) - 32);
} else {
2016-06-27 18:53:15 +02:00
throw new Exception('Got unknown auth_key id');
2016-06-23 23:51:08 +02:00
}
return $data;
}
function method_call($method, $kwargs) {
foreach (pyjslib_range(1, $this->MAX_RETRY) as $i) {
try {
2016-06-27 16:52:28 +02:00
//var_dump(py2php_kwargs_function_call('serialize_method', [$method], $kwargs));
2016-06-27 22:52:07 +02:00
$this->send_message(serialize_method($method, $kwargs));
2016-06-23 23:51:08 +02:00
$server_answer = $this->recv_message();
}
catch(Exception $e) {
2016-06-28 17:21:10 +02:00
echo $e->getMessage() . PHP_EOL;
2016-06-23 23:51:08 +02:00
pyjslib_printnl('Retry call method');
continue;
}
2016-06-28 03:37:04 +02:00
return deserialize(fopen_and_write("php://memory", "w+b", $server_answer));
2016-06-23 23:51:08 +02:00
}
}
function create_auth_key() {
2016-06-26 14:53:16 +02:00
$nonce = random_bytes(16);
2016-06-23 23:51:08 +02:00
pyjslib_printnl('Requesting pq');
2016-06-27 22:52:07 +02:00
$ResPQ = $this->method_call('req_pq', ["nonce" => $nonce]);
2016-06-23 23:51:08 +02:00
$server_nonce = $ResPQ['server_nonce'];
$public_key_fingerprint = $ResPQ['server_public_key_fingerprints'][0];
$pq_bytes = $ResPQ['pq'];
2016-06-26 15:28:39 +02:00
$pq = bytes_to_long($pq_bytes);
list($p, $q) = primefactors($pq);
2016-06-23 23:51:08 +02:00
if (($p > $q)) {
list($p, $q) = [$q, $p];
}
assert((($p * $q) == $pq) && ($p < $q));
pyjslib_printnl(sprintf('Factorization %d = %d * %d', [$pq, $p, $q]));
2016-06-26 15:28:39 +02:00
$p_bytes = long_to_bytes($p);
$q_bytes = long_to_bytes($q);
2016-06-27 18:53:15 +02:00
$f = pyjslib_open(__DIR__ . '/rsa.pub');
2016-06-23 23:51:08 +02:00
$key = RSA::importKey($f->read());
2016-06-26 14:53:16 +02:00
$new_nonce = random_bytes(32);
2016-06-27 16:52:28 +02:00
$data = py2php_kwargs_function_call('serialize_obj', ['p_q_inner_data'], ["pq" => $pq_bytes, "p" => $p_bytes, "q" => $q_bytes, "nonce" => $nonce, "server_nonce" => $server_nonce, "new_nonce" => $new_nonce]);
2016-06-23 23:51:08 +02:00
$sha_digest = sha($data, true);
2016-06-28 03:10:15 +02:00
$random_bytes = random_bytes(((255 - strlen($data)) - strlen($sha_digest)));
2016-06-23 23:51:08 +02:00
$to_encrypt = (($sha_digest + $data) + $random_bytes);
$encrypted_data = $key->encrypt($to_encrypt, 0) [0];
pyjslib_printnl('Starting Diffie Hellman key exchange');
2016-06-27 22:52:07 +02:00
$server_dh_params = $this->method_call('req_DH_params', ["nonce" => $nonce, "server_nonce" => $server_nonce, "p" => $p_bytes, "q" => $q_bytes, "public_key_fingerprint" => $public_key_fingerprint, "encrypted_data" => $encrypted_data]);
2016-06-23 23:51:08 +02:00
assert(($nonce == $server_dh_params['nonce']));
assert(($server_nonce == $server_dh_params['server_nonce']));
$encrypted_answer = $server_dh_params['encrypted_answer'];
$tmp_aes_key = (sha1(($new_nonce + $server_nonce), true) + array_slice(sha1(($server_nonce + $new_nonce), true), 0, 12 - 0));
$tmp_aes_iv = ((array_slice(sha1(($server_nonce + $new_nonce), true), 12, 20 - 12) + sha1(($new_nonce + $new_nonce), true)) + array_slice($new_nonce, 0, 4 - 0));
$answer_with_hash = crypt::ige_decrypt($encrypted_answer, $tmp_aes_key, $tmp_aes_iv);
$answer_hash = array_slice($answer_with_hash, null, 20);
$answer = array_slice($answer_with_hash, 20, null);
2016-06-27 16:52:28 +02:00
$server_DH_inner_data = deserialize(io::BytesIO($answer));
2016-06-23 23:51:08 +02:00
assert(($nonce == $server_DH_inner_data['nonce']));
assert(($server_nonce == $server_DH_inner_data['server_nonce']));
$dh_prime_str = $server_DH_inner_data['dh_prime'];
$g = $server_DH_inner_data['g'];
$g_a_str = $server_DH_inner_data['g_a'];
$server_time = $server_DH_inner_data['server_time'];
2016-06-27 16:52:28 +02:00
$this->timedelta = ($server_time - time());
2016-06-23 23:51:08 +02:00
pyjslib_printnl(sprintf('Server-client time delta = %.1f s', $this->timedelta));
$dh_prime = new bytes_to_long($dh_prime_str);
$g_a = new bytes_to_long($g_a_str);
assert(prime::isprime($dh_prime));
$retry_id = 0;
2016-06-26 14:53:16 +02:00
$b_str = random_bytes(256);
2016-06-23 23:51:08 +02:00
$b = new bytes_to_long($b_str);
$g_b = pow($g, $b, $dh_prime);
$g_b_str = new long_to_bytes($g_b);
2016-06-27 16:52:28 +02:00
$data = py2php_kwargs_function_call('serialize_obj', ['client_DH_inner_data'], ["nonce" => $nonce, "server_nonce" => $server_nonce, "retry_id" => $retry_id, "g_b" => $g_b_str]);
2016-06-23 23:51:08 +02:00
$data_with_sha = (sha1($data, true) + $data);
2016-06-28 03:10:15 +02:00
$data_with_sha_padded = ($data_with_sha + random_bytes((-strlen($data_with_sha) % 16)));
2016-06-23 23:51:08 +02:00
$encrypted_data = crypt::ige_encrypt($data_with_sha_padded, $tmp_aes_key, $tmp_aes_iv);
foreach (pyjslib_range(1, $this->AUTH_MAX_RETRY) as $i) {
2016-06-27 22:52:07 +02:00
$Set_client_DH_params_answer = $this->method_call('set_client_DH_params', ["nonce" => $nonce, "server_nonce" => $server_nonce, "encrypted_data" => $encrypted_data]);
2016-06-23 23:51:08 +02:00
$auth_key = pow($g_a, $b, $dh_prime);
$auth_key_str = new long_to_bytes($auth_key);
$auth_key_sha = sha1($auth_key_str, true);
$auth_key_aux_hash = array_slice($auth_key_sha, null, 8);
$new_nonce_hash1 = array_slice(sha1($new_nonce . '' . $auth_key_aux_hash, true), -16, null);
$new_nonce_hash2 = array_slice(sha1($new_nonce . '' . $auth_key_aux_hash, true), -16, null);
$new_nonce_hash3 = array_slice(sha1($new_nonce . '' . $auth_key_aux_hash, true), -16, null);
assert(($Set_client_DH_params_answer['nonce'] == $nonce));
assert(($Set_client_DH_params_answer['server_nonce'] == $server_nonce));
if (($Set_client_DH_params_answer->name == 'dh_gen_ok')) {
assert(($Set_client_DH_params_answer['new_nonce_hash1'] == $new_nonce_hash1));
pyjslib_printnl('Diffie Hellman key exchange processed successfully');
$this->server_salt = new strxor(array_slice($new_nonce, 0, 8 - 0), array_slice($server_nonce, 0, 8 - 0));
$this->auth_key = $auth_key_str;
$this->auth_key_id = array_slice($auth_key_sha, -8, null);
pyjslib_printnl('Auth key generated');
return 'Auth Ok';
} else if (($Set_client_DH_params_answer->name == 'dh_gen_retry')) {
assert(($Set_client_DH_params_answer['new_nonce_hash2'] == $new_nonce_hash2));
pyjslib_printnl('Retry Auth');
} else if (($Set_client_DH_params_answer->name == 'dh_gen_fail')) {
assert(($Set_client_DH_params_answer['new_nonce_hash3'] == $new_nonce_hash3));
pyjslib_printnl('Auth Failed');
2016-06-27 18:53:15 +02:00
throw new Exception('Auth Failed');
2016-06-23 23:51:08 +02:00
} else {
2016-06-27 18:53:15 +02:00
throw new Exception('Response Error');
2016-06-23 23:51:08 +02:00
}
}
}
function aes_calculate($msg_key, $direction = 'to server') {
$x = ($direction == 'to server') ? 0 : 8;
$sha1_a = sha1(($msg_key + array_slice($this->auth_key, $x, ($x + 32) - $x)), true);
$sha1_b = sha1(((array_slice($this->auth_key, ($x + 32), ($x + 48) - ($x + 32)) + $msg_key) + array_slice($this->auth_key, (48 + $x), (64 + $x) - (48 + $x))), true);
$sha1_c = sha1((array_slice($this->auth_key, ($x + 64), ($x + 96) - ($x + 64)) + $msg_key))->digest();
$sha1_d = sha1(($msg_key + array_slice($this->auth_key, ($x + 96), ($x + 128) - ($x + 96))))->digest();
$aes_key = ((array_slice($sha1_a, 0, 8 - 0) + array_slice($sha1_b, 8, 20 - 8)) + array_slice($sha1_c, 4, 16 - 4));
$aes_iv = (((array_slice($sha1_a, 8, 20 - 8) + array_slice($sha1_b, 0, 8 - 0)) + array_slice($sha1_c, 16, 20 - 16)) + array_slice($sha1_d, 0, 8 - 0));
return [$aes_key, $aes_iv];
}
}