From 9789311157413af30c47ade19651e901730f64b7 Mon Sep 17 00:00:00 2001 From: danogentili Date: Wed, 17 Aug 2016 21:19:54 +0200 Subject: [PATCH] Reorganized code --- src/danog/MadelineProto/API.php | 4 +- src/danog/MadelineProto/Exception.php | 2 +- src/danog/MadelineProto/Session.php | 177 ++++++++++++++++++-------- src/danog/MadelineProto/TL/TL.php | 16 +-- 4 files changed, 134 insertions(+), 65 deletions(-) diff --git a/src/danog/MadelineProto/API.php b/src/danog/MadelineProto/API.php index a89ae829..7de260da 100644 --- a/src/danog/MadelineProto/API.php +++ b/src/danog/MadelineProto/API.php @@ -12,13 +12,13 @@ If not, see . namespace danog\MadelineProto; -class API +class API { public $session; public function __construct($params = []) { - set_error_handler([$this, 'ExceptionErrorHandler']); + set_error_handler(['\danog\MadelineProto\Exception', 'ExceptionErrorHandler']); $this->session = new Session($params); $future_salts = $this->get_future_salts(3); $this->session->log->log($future_salts); diff --git a/src/danog/MadelineProto/Exception.php b/src/danog/MadelineProto/Exception.php index 1bfc4461..542637dc 100644 --- a/src/danog/MadelineProto/Exception.php +++ b/src/danog/MadelineProto/Exception.php @@ -29,7 +29,7 @@ class Exception extends \Exception * * Error handler */ - public function ExceptionErrorHandler($errno = 0, $errstr = null, $errfile = null, $errline = null) + public static function ExceptionErrorHandler($errno = 0, $errstr = null, $errfile = null, $errline = null) { // If error is suppressed with @, don't throw an exception if (error_reporting() === 0) { diff --git a/src/danog/MadelineProto/Session.php b/src/danog/MadelineProto/Session.php index 7ce13b17..0f6e9724 100644 --- a/src/danog/MadelineProto/Session.php +++ b/src/danog/MadelineProto/Session.php @@ -25,10 +25,8 @@ class Session extends Tools $default_settings = [ 'authorization' => [ 'auth_key' => null, - 'auth_key_id' => null, 'temp_auth_key' => null, - 'temp_auth_key_expires_in' => 86400, - 'server_salt' => null, + 'default_temp_auth_key_expires_in' => 86400, 'session_id' => \phpseclib\Crypt\Random::string(8), 'rsa_key' => '-----BEGIN RSA PUBLIC KEY----- MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6 @@ -92,14 +90,17 @@ Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB // Istantiate logging class $this->log = new Logging($this->settings['logging']['logging'], $this->settings['logging']['logging_param']); - $this->connection_seq_no = 0; - $this->seq_no = 0; + $this->connection_seq_no = -1; + $this->seq_no = -1; $this->timedelta = 0; // time delta $this->message_ids = []; - if (($this->settings['authorization']['auth_key'] == null) || ($this->settings['authorization']['server_salt'] == null)) { - $auth_res = $this->create_auth_key(-1); - $temp_auth_res = $this->create_auth_key($this->settings['authorization']['temp_auth_key_expires_in']); + if ($this->settings['authorization']['temp_auth_key'] == null || $this->settings['authorization']['auth_key'] == null) { + if ($this->settings['authorization']['auth_key'] == null) { + $this->settings['authorization']['auth_key'] = $this->create_auth_key(-1); + } + $this->settings['authorization']['temp_auth_key'] = $this->create_auth_key($this->settings['authorization']['default_temp_auth_key_expires_in']); + } } @@ -144,22 +145,22 @@ Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB public function send_message($message_data) { $message_id = $this->struct->pack('timedelta) * pow(2, 30)) * 4); - if (($this->settings['authorization']['auth_key'] == null) || ($this->settings['authorization']['server_salt'] == null)) { + if (($this->settings['authorization']['temp_auth_key']['auth_key'] == null) || ($this->settings['authorization']['temp_auth_key']['server_salt'] == null)) { $message = Tools::string2bin('\x00\x00\x00\x00\x00\x00\x00\x00').$message_id.$this->struct->pack('server_salt.$this->settings['authorization']['session_id'].$message_id.$this->struct->pack('seq_no, strlen($message_data)).$message_data; + $this->seq_no++; + $encrypted_data = $this->settings['authorization']['temp_auth_key']['server_salt'].$this->settings['authorization']['session_id'].$message_id.$this->struct->pack('seq_no, strlen($message_data)).$message_data; $message_key = substr(sha1($encrypted_data, true), -16); $padding = \phpseclib\Crypt\Random::string(Tools::posmod(-strlen($encrypted_data), 16)); list($aes_key, $aes_iv) = $this->aes_calculate($message_key); - $message = $this->settings['authorization']['auth_key_id'].$message_key.Crypt::ige_encrypt($encrypted_data.$padding, $aes_key, $aes_iv); - $this->seq_no++; + $message = $this->settings['authorization']['temp_auth_key']['id'].$message_key.Crypt::ige_encrypt($encrypted_data.$padding, $aes_key, $aes_iv); } switch ($this->settings['connection']['protocol']) { case 'tcp_full': + $this->connection_seq_no++; $step1 = $this->struct->pack('connection_seq_no).$message; $step2 = $step1.$this->struct->pack('newcrc32($step1)); $this->sock->write($step2); - $this->connection_seq_no++; break; case 'tcp_intermediate': $step1 = $this->struct->pack('struct->unpack('connection_seq_no - 1) { + if ($connection_seq_no != $this->connection_seq_no) { throw new Exception('Connection seq_no mismatch'); } $payload = Tools::fopen_and_write('php://memory', 'rw+b', substr($packet, 4, $packet_length - 12)); @@ -235,28 +236,29 @@ Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB list($message_id, $message_length) = $this->struct->unpack('add_check_message_ids($message_id); $data = fread($payload, $message_length); - } elseif ($auth_key_id == $this->settings['authorization']['auth_key_id']) { + } elseif ($auth_key_id == $this->settings['authorization']['temp_auth_key']['id']) { $message_key = fread($payload, 16); $encrypted_data = stream_get_contents($payload); list($aes_key, $aes_iv) = $this->aes_calculate($message_key, 'from server'); $decrypted_data = Tools::fopen_and_write('php://memory', 'rw+b', Crypt::ige_decrypt($encrypted_data, $aes_key, $aes_iv)); $server_salt = fread($decrypted_data, 8); $session_id = fread($decrypted_data, 8); - if ($server_salt != $this->settings['authorization']['server_salt']) { + if ($server_salt != $this->settings['authorization']['temp_auth_key']['server_salt']) { throw new Exception('Server salt mismatch.'); } if ($session_id != $this->settings['authorization']['session_id']) { throw new Exception('Session id mismatch.'); } - $message_id = fread($decrypted_data, 8); + $message_id = $this->struct->unpack('add_check_message_ids($message_id); $seq_no = $this->struct->unpack('seq_no); if ($seq_no != $this->seq_no) { throw new Exception('Seq_no mismatch'); } $message_data_length = $this->struct->unpack('struct->pack('>I', (string) $q); $new_nonce = \phpseclib\Crypt\Random::string(32); - if ($expires_in == -1) { + if ($expires_in < 0) { $data = $this->tl->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]); } else { $data = $this->tl->serialize_obj('p_q_inner_data_temp', ['pq' => $pq_bytes, 'p' => $p_bytes, 'q' => $q_bytes, 'nonce' => $nonce, 'server_nonce' => $server_nonce, 'new_nonce' => $new_nonce, 'expires_in' => $expires_in]); @@ -351,55 +353,123 @@ Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB 'encrypted_data' => $encrypted_data, ] ); - + // Check nonce and server_nonce if ($nonce != $server_dh_params['nonce']) { throw new Exception('Handshake: wrong nonce.'); } if ($server_nonce != $server_dh_params['server_nonce']) { throw new Exception('Handshake: wrong server nonce.'); } + if (isset($server_dh_params["new_nonce_hash"]) && substr(sha1($new_nonce), -32) != $server_dh_params["new_nonce_hash"]) { + throw new Exception('Handshake: wrong new nonce hash.'); + } + + // Get key and iv and decrypt answer $encrypted_answer = $server_dh_params['encrypted_answer']; $tmp_aes_key = sha1($new_nonce.$server_nonce, true).substr(sha1($server_nonce.$new_nonce, true), 0, 12); $tmp_aes_iv = substr(sha1($server_nonce.$new_nonce, true), 12, 8).sha1($new_nonce.$new_nonce, true).substr($new_nonce, 0, 4); $answer_with_hash = Crypt::ige_decrypt($encrypted_answer, $tmp_aes_key, $tmp_aes_iv); + + // Separate answer and hash $answer_hash = substr($answer_with_hash, 0, 20); $answer = substr($answer_with_hash, 20); + + // Deserialize $server_DH_inner_data = $this->tl->deserialize(Tools::fopen_and_write('php://memory', 'rw+b', $answer)); + + // Time delta + $server_time = $server_DH_inner_data['server_time']; + $this->timedelta = ($server_time - time()); + $this->log->log(sprintf('Server-client time delta = %.1f s', $this->timedelta)); + + // Do some checks + $server_DH_inner_data_length = $this->tl->get_length(Tools::fopen_and_write('php://memory', 'rw+b', $answer)); + if (sha1(substr($answer, 0, $server_DH_inner_data_length), true) != $answer_hash) { + throw new Exception('Handshake: answer_hash mismatch.'); + } if ($nonce != $server_DH_inner_data['nonce']) { throw new Exception('Handshake: wrong nonce'); } if ($server_nonce != $server_DH_inner_data['server_nonce']) { throw new Exception('Handshake: wrong server nonce'); } - $dh_prime_str = $server_DH_inner_data['dh_prime']; $g = new \phpseclib\Math\BigInteger($server_DH_inner_data['g']); - $g_a_str = $server_DH_inner_data['g_a']; - $server_time = $server_DH_inner_data['server_time']; - $this->timedelta = ($server_time - time()); - $this->log->log(sprintf('Server-client time delta = %.1f s', $this->timedelta)); - $dh_prime = new \phpseclib\Math\BigInteger($dh_prime_str, 256); - $g_a = new \phpseclib\Math\BigInteger($g_a_str, 256); + $g_a = new \phpseclib\Math\BigInteger($server_DH_inner_data['g_a'], 256); + $dh_prime = new \phpseclib\Math\BigInteger($server_DH_inner_data['dh_prime'], 256); + + // Define some needed numbers for BigInteger + $one = new \phpseclib\Math\BigInteger(1); + $two = new \phpseclib\Math\BigInteger(2); + $twoe2047 = new \phpseclib\Math\BigInteger("16158503035655503650357438344334975980222051334857742016065172713762327569433945446598600705761456731844358980460949009747059779575245460547544076193224141560315438683650498045875098875194826053398028819192033784138396109321309878080919047169238085235290822926018152521443787945770532904303776199561965192760957166694834171210342487393282284747428088017663161029038902829665513096354230157075129296432088558362971801859230928678799175576150822952201848806616643615613562842355410104862578550863465661734839271290328348967522998634176499319107762583194718667771801067716614802322659239302476074096777926805529798115328"); + $twoe2048 = new \phpseclib\Math\BigInteger("32317006071311007300714876688669951960444102669715484032130345427524655138867890893197201411522913463688717960921898019494119559150490921095088152386448283120630877367300996091750197750389652106796057638384067568276792218642619756161838094338476170470581645852036305042887575891541065808607552399123930385521914333389668342420684974786564569494856176035326322058077805659331026192708460314150258592864177116725943603718461857357598351152301645904403697613233287231227125684710820209725157101726931323469678542580656697935045997268352998638215525166389437335543602135433229604645318478604952148193555853611059596230656"); + + // Check validity of dh_prime if (!$dh_prime->isPrime()) { - throw new Exception("Handshake: dh_prime isn't a prime."); + throw new Exception("Handshake: dh_prime isn't a safe 2048-bit prime (dh_prime isn't a prime)."); } - $retry_id = 0; - $b_str = \phpseclib\Crypt\Random::string(256); - $b = new \phpseclib\Math\BigInteger($b_str, 256); - $g_b = $g->powMod($b, $dh_prime); - $g_b_str = $g_b->toBytes(); - $data = $this->tl->serialize_obj('client_DH_inner_data', ['nonce' => $nonce, 'server_nonce' => $server_nonce, 'retry_id' => $retry_id, 'g_b' => $g_b_str]); - $data_with_sha = sha1($data, true).$data; - $data_with_sha_padded = $data_with_sha.\phpseclib\Crypt\Random::string(Tools::posmod(-strlen($data_with_sha), 16)); - $encrypted_data = Crypt::ige_encrypt($data_with_sha_padded, $tmp_aes_key, $tmp_aes_iv); - foreach (Tools::range(1, $this->settings['max_tries']['authorization']) as $i) { + /* + // Almost always fails + if (!$dh_prime->subtract($one)->divide($two)[0]->isPrime()) { + throw new Exception("Handshake: dh_prime isn't a safe 2048-bit prime ((dh_prime - 1) / 2 isn't a prime)."); + } + */ + // 2^2047 < dh_prime < 2^2048 + if ($dh_prime->compare($twoe2047) <= 0 // 2^2047 < dh_prime or dh_prime > 2^2047 or ! dh_prime <= 2^2047 + || $dh_prime->compare($twoe2048) >= 0 // dh_prime < 2^2048 or ! dh_prime >= 2^2048 + ) { + throw new Exception("Handshake: g isn't a safe 2048-bit prime (2^2047 < dh_prime < 2^2048 is false)."); + } + + // Check validity of g + // 1 < g < dh_prime - 1 + if ($g->compare($one) <= 0 // 1 < g or g > 1 or ! g <= 1 + || $g->compare($dh_prime->subtract($one)) >= 0 // g < dh_prime - 1 or ! g >= dh_prime - 1 + ) { + throw new Exception("Handshake: g is invalid (1 < g < dh_prime - 1 is false)."); + } + + // Check validity of g_a + // 1 < g_a < dh_prime - 1 + if ($g_a->compare($one) <= 0 // 1 < g_a or g_a > 1 or ! g_a <= 1 + || $g_a->compare($dh_prime->subtract($one)) >= 0 // g_a < dh_prime - 1 or ! g_a >= dh_prime - 1 + ) { + throw new Exception("Handshake: g_a is invalid (1 < g_a < dh_prime - 1 is false)."); + } + + foreach (Tools::range(0, $this->settings['max_tries']['authorization']) as $retry_id) { + $b = new \phpseclib\Math\BigInteger(\phpseclib\Crypt\Random::string(256), 256); + $g_b = $g->powMod($b, $dh_prime); + + // Check validity of g_b + // 1 < g_b < dh_prime - 1 + if ($g_b->compare($one) <= 0 // 1 < g_b or g_b > 1 or ! g_b <= 1 + || $g_b->compare($dh_prime->subtract($one)) >= 0 // g_b < dh_prime - 1 or ! g_b >= dh_prime - 1 + ) { + throw new Exception("Handshake: g_b is invalid (1 < g_b < dh_prime - 1 is false)."); + } + + $g_b_str = $g_b->toBytes(); + + // serialize client_DH_inner_data + $data = $this->tl->serialize_obj('client_DH_inner_data', ['nonce' => $nonce, 'server_nonce' => $server_nonce, 'retry_id' => $retry_id, 'g_b' => $g_b_str]); + $data_with_sha = sha1($data, true).$data; + $data_with_sha_padded = $data_with_sha.\phpseclib\Crypt\Random::string(Tools::posmod(-strlen($data_with_sha), 16)); + + // encrypt client_DH_inner_data + $encrypted_data = Crypt::ige_encrypt($data_with_sha_padded, $tmp_aes_key, $tmp_aes_iv); + + // Send set_client_DH_params query $Set_client_DH_params_answer = $this->method_call('set_client_DH_params', ['nonce' => $nonce, 'server_nonce' => $server_nonce, 'encrypted_data' => $encrypted_data]); + + // Generate auth_key $auth_key = $g_a->powMod($b, $dh_prime); $auth_key_str = $auth_key->toBytes(); $auth_key_sha = sha1($auth_key_str, true); $auth_key_aux_hash = substr($auth_key_sha, 0, 8); - $new_nonce_hash1 = substr(sha1($new_nonce.''.$auth_key_aux_hash, true), -16); - $new_nonce_hash2 = substr(sha1($new_nonce.''.$auth_key_aux_hash, true), -16); - $new_nonce_hash3 = substr(sha1($new_nonce.''.$auth_key_aux_hash, true), -16); + $new_nonce_hash1 = substr(sha1($new_nonce.chr(1).$auth_key_aux_hash, true), -16); + $new_nonce_hash2 = substr(sha1($new_nonce.chr(2).$auth_key_aux_hash, true), -16); + $new_nonce_hash3 = substr(sha1($new_nonce.chr(3).$auth_key_aux_hash, true), -16); + if ($Set_client_DH_params_answer['nonce'] != $nonce) { throw new Exception('Handshake: wrong nonce.'); } @@ -411,17 +481,22 @@ Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB throw new Exception('Handshake: wrong new_nonce_hash1'); } $this->log->log('Diffie Hellman key exchange processed successfully'); - $server_salt = substr($new_nonce, 0, 8 - 0) ^ substr($server_nonce, 0, 8 - 0); - $auth_key = $auth_key_str; - $auth_key_id = substr($auth_key_sha, -8); - $this->log->log('Auth key generated'); - return ['auth_key' => $auth_key, 'auth_key_id' => $auth_key_id, 'server_salt', $server_salt]; + $res_authorization = [ "server_salt" => substr($new_nonce, 0, 8 - 0) ^ substr($server_nonce, 0, 8 - 0) ]; + $res_authorization["auth_key"] = $auth_key_str; + $res_authorization["id"] = substr($auth_key_sha, -8); + if ($expires_in < 0) { + $res_authorization["expires_in"] = $expires_in; + } + $this->log->log('Auth key generated'); + $this->timedelta = 0; + + return $res_authorization; } elseif ($Set_client_DH_params_answer['_'] == 'dh_gen_retry') { if ($Set_client_DH_params_answer['new_nonce_hash2'] != $new_nonce_hash2) { throw new Exception('Handshake: wrong new_nonce_hash_2'); } - $this->log->log('Retry Auth'); + $this->log->log('Retrying Auth'); } elseif ($Set_client_DH_params_answer['_'] == 'dh_gen_fail') { if ($Set_client_DH_params_answer['new_nonce_hash3'] != $new_nonce_hash3) { throw new Exception('Handshake: wrong new_nonce_hash_3'); @@ -438,10 +513,10 @@ Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB public function aes_calculate($msg_key, $direction = 'to server') { $x = ($direction == 'to server') ? 0 : 8; - $sha1_a = sha1($msg_key.substr($this->settings['authorization']['auth_key'], $x, ($x + 32) - $x), true); - $sha1_b = sha1(substr($this->settings['authorization']['auth_key'], ($x + 32), ($x + 48) - ($x + 32)).$msg_key.substr($this->settings['authorization']['auth_key'], (48 + $x), (64 + $x) - (48 + $x)), true); - $sha1_c = sha1(substr($this->settings['authorization']['auth_key'], ($x + 64), ($x + 96) - ($x + 64)).$msg_key, true); - $sha1_d = sha1($msg_key.substr($this->settings['authorization']['auth_key'], ($x + 96), ($x + 128) - ($x + 96)), true); + $sha1_a = sha1($msg_key.substr($this->settings['authorization']['temp_auth_key']['auth_key'], $x, ($x + 32) - $x), true); + $sha1_b = sha1(substr($this->settings['authorization']['temp_auth_key']['auth_key'], ($x + 32), ($x + 48) - ($x + 32)).$msg_key.substr($this->settings['authorization']['temp_auth_key']['auth_key'], (48 + $x), (64 + $x) - (48 + $x)), true); + $sha1_c = sha1(substr($this->settings['authorization']['temp_auth_key']['auth_key'], ($x + 64), ($x + 96) - ($x + 64)).$msg_key, true); + $sha1_d = sha1($msg_key.substr($this->settings['authorization']['temp_auth_key']['auth_key'], ($x + 96), ($x + 128) - ($x + 96)), true); $aes_key = substr($sha1_a, 0, 8 - 0).substr($sha1_b, 8, 20 - 8).substr($sha1_c, 4, 16 - 4); $aes_iv = substr($sha1_a, 8, 20 - 8).substr($sha1_b, 0, 8 - 0).substr($sha1_c, 16, 20 - 16).substr($sha1_d, 0, 8 - 0); diff --git a/src/danog/MadelineProto/TL/TL.php b/src/danog/MadelineProto/TL/TL.php index 19597cfe..c43c63c2 100644 --- a/src/danog/MadelineProto/TL/TL.php +++ b/src/danog/MadelineProto/TL/TL.php @@ -131,22 +131,16 @@ class TL } } - public function deserialize($bytes_io, $type_ = null, $subtype = null) - { - return $this->deserialize_length($bytes_io, $type_, $subtype); - } - public function get_length($bytes_io, $type_ = null, $subtype = null) { - $this->deserialize_length($bytes_io, $type_, $subtype); - + $this->deserialize($bytes_io, $type_, $subtype); return ftell($bytes_io); } /** * :type bytes_io: io.BytesIO object. */ - public function deserialize_length($bytes_io, $type_ = null, $subtype = null) + public function deserialize($bytes_io, $type_ = null, $subtype = null) { if (!(get_resource_type($bytes_io) == 'file' || get_resource_type($bytes_io) == 'stream')) { throw new Exception('An invalid bytes_io handle provided.'); @@ -201,7 +195,7 @@ class TL $count = \danog\PHP\Struct::unpack('deserialize_length($bytes_io, $subtype); + $x[] = $this->deserialize($bytes_io, $subtype); } break; default: @@ -219,11 +213,11 @@ class TL $base_boxed_types = ['Vector t', 'Int', 'Long', 'Double', 'String', 'Int128', 'Int256']; if (in_array($tl_elem->type, $base_boxed_types)) { - $x = $this->deserialize_length($bytes_io, $tl_elem->predicate, $subtype); + $x = $this->deserialize($bytes_io, $tl_elem->predicate, $subtype); } else { $x = ['_' => $tl_elem->predicate]; foreach ($tl_elem->params as $arg) { - $x[$arg['name']] = $this->deserialize_length($bytes_io, $arg['type'], $arg['subtype']); + $x[$arg['name']] = $this->deserialize($bytes_io, $arg['type'], $arg['subtype']); } } break;