Improved 2FA

This commit is contained in:
Daniil Gentili 2019-06-30 15:12:43 +02:00
parent 6b5775da79
commit 37a9133a6f
8 changed files with 194 additions and 151 deletions

View File

@ -144,6 +144,7 @@ Tip: if you receive an error (or nothing), [send us](https://t.me/pwrtelegramgro
* [Full chat info with full list of participants](https://docs.madelineproto.xyz/docs/CHAT_INFO.html#get_pwr_chat-now-fully-async)
* [Full chat info](https://docs.madelineproto.xyz/docs/CHAT_INFO.html#get_full_info-now-fully-async)
* [Reduced chat info (very fast)](https://docs.madelineproto.xyz/docs/CHAT_INFO.html#get_info-now-fully-async)
* [Just the chat ID (extremely fast)](https://docs.madelineproto.xyz/docs/CHAT_INFO.html#get_id-now-fully-async)
* [Getting all chats (dialogs)](https://docs.madelineproto.xyz/docs/DIALOGS.html)
* [Dialog list](https://docs.madelineproto.xyz/docs/DIALOGS.html#get_dialogs-now-fully-async)
* [Full dialog info](https://docs.madelineproto.xyz/docs/DIALOGS.html#get_full_dialogs-now-fully-async)

2
docs

@ -1 +1 @@
Subproject commit 7414ae3e537b26a15b75d2a00ef6f93e702d2cd8
Subproject commit ddb2e4f76938b69ceac6e4615901c642accae1ef

View File

@ -390,6 +390,8 @@ trait AuthKeyHandler
$req_pq = $req_pq === 'req_pq_multi' ? 'req_pq' : 'req_pq_multi';
} catch (\danog\MadelineProto\RPCErrorException $e) {
$this->logger->logger('An RPCErrorException occurred while generating the authorization key: '.$e->getMessage().' Retrying (try number '.$retry_id_total.')...', \danog\MadelineProto\Logger::WARNING);
} catch (\Throwable $e) {
$this->logger->logger('An exception occurred while generating the authorization key: '.$e.PHP_EOL.' Retrying (try number '.$retry_id_total.')...', \danog\MadelineProto\Logger::WARNING);
}
}
if (strpos($datacenter, 'cdn') === false) {

View File

@ -25,24 +25,77 @@ use danog\MadelineProto\Tools;
use phpseclib\Math\BigInteger;
/**
* Manages password calculation.
* Manages SRP password calculation
*
* @author Daniil Gentili <daniil@daniil.it>
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
class PasswordCalculator
{
use AuthKeyHandler;
use Tools;
/**
* The algorithm to use for calculating the hash of new passwords (a PasswordKdfAlgo object)
*
* @var array
*/
private $new_algo;
/**
* A secure random string that can be used to compute the password
*
* @var string
*/
private $secure_random = '';
/**
* The algorithm to use for calculatuing the hash of the current password (a PasswordKdfAlgo object)
*
* @var array
*/
private $current_algo;
/**
* SRP b parameter
*
* @var BigInteger
*/
private $srp_B;
/**
* SRP b parameter for hashing
*
* @var BigInteger
*/
private $srp_BForHash;
/**
* SRP ID
*
* @var [type]
*/
private $srp_id;
/**
* Logger
*
* @var \danog\MadelineProto\Logger
*/
public $logger;
// This is needed do not remove this
public function __construct($logger) { $this->logger = $logger; }
/**
* Initialize logger
*
* @param \danog\MadelineProto\Logger $logger
*/
public function __construct($logger)
{
$this->logger = $logger;
}
/**
* Popupate 2FA configuration
*
* @param array $object 2FA configuration object obtained using account.getPassword
* @return void
*/
public function addInfo(array $object)
{
if ($object['_'] !== 'account.password') {
@ -101,16 +154,39 @@ class PasswordCalculator
$this->secure_random = $object['secure_random'];
}
/**
* Create a random string (eventually prefixed by the specified string)
*
* @param string $prefix Prefix
* @return string Salt
*/
public function createSalt(string $prefix = ''): string
{
return $prefix.$this->random(32);
}
/**
* Hash specified data using the salt with SHA256
*
* The result will be the SHA256 hash of the salt concatenated with the data concatenated with the salt
*
* @param string $data Data to hash
* @param string $salt Salt
* @return string Hash
*/
public function hashSha256(string $data, string $salt): string
{
return hash('sha256', $salt.$data.$salt, true);
}
/**
* Hashes the specified password
*
* @param string $password Password
* @param string $client_salt Client salt
* @param string $server_salt Server salt
* @return string Resulting hash
*/
public function hashPassword(string $password, string $client_salt, string $server_salt): string
{
$buf = $this->hashSha256($password, $client_salt);
@ -120,9 +196,15 @@ class PasswordCalculator
return $this->hashSha256($hash, $server_salt);
}
/**
* Get the InputCheckPassword object for checking the validity of a password using account.checkPassword
*
* @param string $password The password
* @return array InputCheckPassword object
*/
public function getCheckPassword(string $password): array
{
if ($password === '') {
if ($password === '' || !$this->current_algo) {
return ['_' => 'inputCheckPasswordEmpty'];
}
$client_salt = $this->current_algo['salt1'];
@ -166,9 +248,20 @@ class PasswordCalculator
return ['_' => 'inputCheckPasswordSRP', 'srp_id' => $id, 'A' => $AForHash, 'M1' => $M1];
}
/**
* Get parameters to be passed to the account.updatePasswordSettings to update/set a 2FA password
*
* The input params array can contain password, new_password, email and hint params.
*
* @param array $params Input params
* @return array account.updatePasswordSettings parameters
*/
public function getPassword(array $params): array
{
$return = ['password' => $this->getCheckPassword(isset($params['password']) ? $params['password'] : ''), 'new_settings' => ['_' => 'account.passwordInputSettings', 'new_algo' => ['_' => 'passwordKdfAlgoUnknown'], 'new_password_hash' => '', 'hint' => '']];
$oldPassword = $this->getCheckPassword($params['password'] ?? '');
$return = ['password' => $oldPassword, 'new_settings' => ['_' => 'account.passwordInputSettings', 'new_algo' => ['_' => 'passwordKdfAlgoUnknown'], 'new_password_hash' => '', 'hint' => '']];
$new_settings = &$return['new_settings'];
if (isset($params['new_password']) && $params['new_password'] !== '') {
@ -183,11 +276,11 @@ class PasswordCalculator
$vForHash = str_pad($v->toBytes(), 256, chr(0), \STR_PAD_LEFT);
$new_settings['new_algo'] = [
'_' => 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow',
'_' => 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow',
'salt1' => $client_salt,
'salt2' => $server_salt,
'g' => (int) $g->toString(),
'p' => $pForHash,
'g' => (int) $g->toString(),
'p' => $pForHash,
];
$new_settings['new_password_hash'] = $vForHash;
$new_settings['hint'] = $params['hint'];

View File

@ -175,6 +175,12 @@ class Magic
//$this->logger->logger('Could not enable PHP logging');
}
}
$res = json_decode(@file_get_contents('https://rpc.pwrtelegram.xyz/?allv3'), true);
if (isset($res['ok']) && $res['ok']) {
RPCErrorException::$errorMethodMap = $res['result'];
RPCErrorException::$descriptions += $res['human_result'];
}
self::$inited = true;
}
}

View File

@ -25,21 +25,86 @@ class RPCErrorException extends \Exception
private $fetched = false;
public static $rollbar = true;
public function getMess()
public static $descriptions = [
'RPC_MCGET_FAIL' => 'Telegram is having internal issues, please try again later.',
'RPC_CALL_FAIL' => 'Telegram is having internal issues, please try again later.',
'USER_PRIVACY_RESTRICTED' => "The user's privacy settings do not allow you to do this",
'CHANNEL_PRIVATE' => "You haven't joined this channel/supergroup",
'USER_IS_BOT' => "Bots can't send messages to other bots",
'BOT_METHOD_INVALID' => 'This method cannot be run by a bot',
'PHONE_CODE_EXPIRED' => 'The phone code you provided has expired, this may happen if it was sent to any chat on telegram (if the code is sent through a telegram chat (not the official account) to avoid it append or prepend to the code some chars)',
'USERNAME_INVALID' => 'The provided username is not valid',
'ACCESS_TOKEN_INVALID' => 'The provided token is not valid',
'ACTIVE_USER_REQUIRED' => 'The method is only available to already activated users',
'FIRSTNAME_INVALID' => 'The first name is invalid',
'LASTNAME_INVALID' => 'The last name is invalid',
'PHONE_NUMBER_INVALID' => 'The phone number is invalid',
'PHONE_CODE_HASH_EMPTY' => 'phone_code_hash is missing',
'PHONE_CODE_EMPTY' => 'phone_code is missing',
'PHONE_CODE_EXPIRED' => 'The confirmation code has expired',
'API_ID_INVALID' => 'The api_id/api_hash combination is invalid',
'PHONE_NUMBER_OCCUPIED' => 'The phone number is already in use',
'PHONE_NUMBER_UNOCCUPIED' => 'The phone number is not yet being used',
'USERS_TOO_FEW' => 'Not enough users (to create a chat, for example)',
'USERS_TOO_MUCH' => 'The maximum number of users has been exceeded (to create a chat, for example)',
'TYPE_CONSTRUCTOR_INVALID' => 'The type constructor is invalid',
'FILE_PART_INVALID' => 'The file part number is invalid',
'FILE_PARTS_INVALID' => 'The number of file parts is invalid',
'MD5_CHECKSUM_INVALID' => 'The MD5 checksums do not match',
'PHOTO_INVALID_DIMENSIONS' => 'The photo dimensions are invalid',
'FIELD_NAME_INVALID' => 'The field with the name FIELD_NAME is invalid',
'FIELD_NAME_EMPTY' => 'The field with the name FIELD_NAME is missing',
'MSG_WAIT_FAILED' => 'A waiting call returned an error',
'USERNAME_NOT_OCCUPIED' => 'The provided username is not occupied',
'PHONE_NUMBER_BANNED' => 'The provided phone number is banned from telegram',
'AUTH_KEY_UNREGISTERED' => 'The authorization key has expired',
'INVITE_HASH_EXPIRED' => 'The invite link has expired',
'USER_DEACTIVATED' => 'The user was deactivated',
'USER_ALREADY_PARTICIPANT' => 'The user is already in the group',
'MESSAGE_ID_INVALID' => 'The provided message id is invalid',
'PEER_ID_INVALID' => 'The provided peer id is invalid',
'CHAT_ID_INVALID' => 'The provided chat id is invalid',
'MESSAGE_DELETE_FORBIDDEN' => "You can't delete one of the messages you tried to delete, most likely because it is a service message.",
'CHAT_ADMIN_REQUIRED' => 'You must be an admin in this chat to do this',
-429 => 'Too many requests',
'PEER_FLOOD' => "You are spamreported, you can't do this",
];
public static $errorMethodMap = [];
private $caller = '';
public static function localizeMessage($method, $code, $error)
{
if ($this->fetched === false) {
$res = json_decode(@file_get_contents('https://rpc.pwrtelegram.xyz/?method='.$additional[0].'&code='.$code.'&error='.$this->rpc), true);
if (!$method || !$code || !$error) {
return $error;
}
$error = preg_replace('/\d+$/', "X", $error);
$description = self::$descriptions[$error] ?? '';
if (!isset(self::$errorMethodMap[$code][$method][$error])
|| !isset(self::$descriptions[$error])
|| $code === 500
) {
$res = json_decode(@file_get_contents('https://rpc.pwrtelegram.xyz/?method='.$this->caller.'&code='.$this->code.'&error='.$this->rpc), true);
if (isset($res['ok']) && $res['ok']) {
$this->message = $res['result'];
$description = $res['result'];
self::$descriptions[$error] = $description;
self::$errorMethodMap[$code][$method][$error] = $error;
}
}
return $this->message;
if (!$description) {
return $error;
}
return $description;
}
public function __toString()
{
$result = sprintf(\danog\MadelineProto\Lang::$current_lang['rpc_tg_error'], $this->getMess()." ({$this->code})", $this->rpc, $this->file, $this->line.PHP_EOL, \danog\MadelineProto\Magic::$revision.PHP_EOL.PHP_EOL).PHP_EOL.$this->getTLTrace().PHP_EOL;
$result = sprintf(\danog\MadelineProto\Lang::$current_lang['rpc_tg_error'], self::localizeMessage($this->caller, $this->code, $this->message)." ({$this->code})", $this->rpc, $this->file, $this->line.PHP_EOL, \danog\MadelineProto\Magic::$revision.PHP_EOL.PHP_EOL).PHP_EOL.$this->getTLTrace().PHP_EOL;
if (php_sapi_name() !== 'cli') {
$result = str_replace(PHP_EOL, '<br>'.PHP_EOL, $result);
}
@ -50,147 +115,19 @@ class RPCErrorException extends \Exception
public function __construct($message = null, $code = 0, $caller = '', Exception $previous = null)
{
$this->rpc = $message;
switch ($message) {
case 'RPC_MCGET_FAIL':
case 'RPC_CALL_FAIL':
$message = 'Telegram is having internal issues, please try again later.';
break;
case 'USER_PRIVACY_RESTRICTED':
$message = "The user's privacy settings do not allow you to do this";
break;
case 'CHANNEL_PRIVATE':
$message = "You haven't joined this channel/supergroup";
break;
case 'FLOOD_WAIT_666':
$message = 'Spooky af m8';
break;
case 'USER_IS_BOT':
$message = "Bots can't send messages to other bots";
break;
case 'BOT_METHOD_INVALID':
$message = 'This method cannot be run by a bot';
break;
case 'PHONE_CODE_EXPIRED':
$message = 'The phone code you provided has expired, this may happen if it was sent to any chat on telegram (if the code is sent through a telegram chat (not the official account) to avoid it append or prepend to the code some chars)';
break;
case 'USERNAME_INVALID':
$message = 'The provided username is not valid';
break;
case 'ACCESS_TOKEN_INVALID':
$message = 'The provided token is not valid';
break;
case 'ACTIVE_USER_REQUIRED':
$message = 'The method is only available to already activated users';
break;
case 'FIRSTNAME_INVALID':
$message = 'The first name is invalid';
break;
case 'LASTNAME_INVALID':
$message = 'The last name is invalid';
break;
case 'PHONE_NUMBER_INVALID':
$message = 'The phone number is invalid';
break;
case 'PHONE_CODE_HASH_EMPTY':
$message = 'phone_code_hash is missing';
break;
case 'PHONE_CODE_EMPTY':
$message = 'phone_code is missing';
break;
case 'PHONE_CODE_EXPIRED':
$message = 'The confirmation code has expired';
break;
case 'API_ID_INVALID':
$message = 'The api_id/api_hash combination is invalid';
break;
case 'PHONE_NUMBER_OCCUPIED':
$message = 'The phone number is already in use';
break;
case 'PHONE_NUMBER_UNOCCUPIED':
$message = 'The phone number is not yet being used';
break;
case 'USERS_TOO_FEW':
$message = 'Not enough users (to create a chat, for example)';
break;
case 'USERS_TOO_MUCH':
$message = 'The maximum number of users has been exceeded (to create a chat, for example)';
break;
case 'TYPE_CONSTRUCTOR_INVALID':
$message = 'The type constructor is invalid';
break;
case 'FILE_PART_INVALID':
$message = 'The file part number is invalid';
break;
case 'FILE_PARTS_INVALID':
$message = 'The number of file parts is invalid';
break;
case 'MD5_CHECKSUM_INVALID':
$message = 'The MD5 checksums do not match';
break;
case 'PHOTO_INVALID_DIMENSIONS':
$message = 'The photo dimensions are invalid';
break;
case 'FIELD_NAME_INVALID':
$message = 'The field with the name FIELD_NAME is invalid';
break;
case 'FIELD_NAME_EMPTY':
$message = 'The field with the name FIELD_NAME is missing';
break;
case 'MSG_WAIT_FAILED':
$message = 'A waiting call returned an error';
break;
case 'USERNAME_NOT_OCCUPIED':
$message = 'The provided username is not occupied';
break;
case 'PHONE_NUMBER_BANNED':
$message = 'The provided phone number is banned from telegram';
break;
case 'AUTH_KEY_UNREGISTERED':
$message = 'The authorization key has expired';
break;
case 'INVITE_HASH_EXPIRED':
$message = 'The invite link has expired';
break;
case 'USER_DEACTIVATED':
$message = 'The user was deactivated';
break;
case 'USER_ALREADY_PARTICIPANT':
$message = 'The user is already in the group';
break;
case 'MESSAGE_ID_INVALID':
$message = 'The provided message id is invalid';
break;
case 'PEER_ID_INVALID':
$message = 'The provided peer id is invalid';
break;
case 'CHAT_ID_INVALID':
$message = 'The provided chat id is invalid';
break;
case 'MESSAGE_DELETE_FORBIDDEN':
$message = "You can't delete one of the messages you tried to delete, most likely because it is a service message.";
break;
case 'CHAT_ADMIN_REQUIRED':
$message = 'You must be an admin in this chat to do this';
break;
case -429:
case 'PEER_FLOOD':
$message = 'Too many requests';
break;
}
parent::__construct($message, $code, $previous);
$this->prettify_tl($caller);
$this->caller = $caller;
$additional = [];
foreach ($this->getTrace() as $level) {
if (isset($level['function']) && $level['function'] === 'method_call') {
$this->line = $level['line'];
$this->file = $level['file'];
$additional = $level['args'];
break;
}
}
if ($this->rpc !== $message) {
$this->fetched = true;
}
if (!self::$rollbar || !class_exists('\\Rollbar\\Rollbar')) {
return;
}

View File

@ -205,7 +205,7 @@ trait Login
/**
* Update the 2FA password
*
* The params can contain password, new_password, email and hint params.
* The params array can contain password, new_password, email and hint params.
*
* @param array $params The params
* @return void

View File

@ -1,5 +1,7 @@
#!/usr/bin/env php
<?php
use danog\MadelineProto\RPCErrorException;
/*
Copyright 2016-2019 Daniil Gentili
(https://daniil.it)
@ -43,6 +45,7 @@ if (getenv('TEST_SECRET_CHAT') == '') {
echo 'Loading settings...'.PHP_EOL;
$settings = json_decode(getenv('MTPROTO_SETTINGS'), true) ?: [];
/*
* Load MadelineProto
*/
@ -58,6 +61,7 @@ try {
$MadelineProto->accept_tos();
}
}
//var_dump(count($MadelineProto->get_pwr_chat('@madelineproto')['participants']));
/*
@ -78,7 +82,7 @@ $message = (getenv('TRAVIS_COMMIT') == '') ? 'I iz works always (io laborare sem
/*
* Try making a phone call
*/
if (!getenv('TRAVIS_COMMIT') && stripos($MadelineProto->readline('Do you want to make a call? (y/n): '), 'y') !== false) {
if (!getenv('TRAVIS_COMMIT') && stripos(readline('Do you want to make a call? (y/n): '), 'y') !== false) {
$controller = $MadelineProto->request_call(getenv('TEST_SECRET_CHAT'))->play('input.raw')->then('input.raw')->playOnHold(['input.raw'])->setOutputFile('output.raw');
while ($controller->getCallState() < \danog\MadelineProto\VoIP::CALL_STATE_READY) {
$MadelineProto->get_updates();
@ -92,8 +96,8 @@ if (!getenv('TRAVIS_COMMIT') && stripos($MadelineProto->readline('Do you want to
/*
* Try receiving a phone call
*/
if (!getenv('TRAVIS_COMMIT') && stripos($MadelineProto->readline('Do you want to handle incoming calls? (y/n): '), 'y') !== false) {
$howmany = $MadelineProto->readline('How many calls would you like me to handle? ');
if (!getenv('TRAVIS_COMMIT') && stripos(readline('Do you want to handle incoming calls? (y/n): '), 'y') !== false) {
$howmany = readline('How many calls would you like me to handle? ');
$offset = 0;
while ($howmany > 0) {
$updates = $MadelineProto->get_updates(['offset' => $offset, 'limit' => 50, 'timeout' => 0]); // Just like in the bot API, you can specify an offset, a limit and a timeout
@ -114,7 +118,7 @@ if (!getenv('TRAVIS_COMMIT') && stripos($MadelineProto->readline('Do you want to
/*
* Secret chat usage
*/
if (!getenv('TRAVIS_COMMIT') && stripos($MadelineProto->readline('Do you want to make the secret chat tests? (y/n): '), 'y') !== false) {
if (!getenv('TRAVIS_COMMIT') && stripos(readline('Do you want to make the secret chat tests? (y/n): '), 'y') !== false) {
/**
* Request a secret chat.
*/