. * * @author Daniil Gentili * @copyright 2016-2023 Daniil Gentili * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 * @link https://docs.madelineproto.xyz MadelineProto documentation */ namespace danog\MadelineProto; use danog\MadelineProto\MTProtoTools\Crypt; use PDO; use Webmozart\Assert\Assert; use const DIRECTORY_SEPARATOR; use function stream_get_contents; final class Conversion { /** * Prepare API instance. * * @param array $authorization Authorization info */ public static function importAuthorization(array $authorization, int $main_dc_id, string $session, SettingsAbstract|array $settings): API { $settings = Settings::parseFromLegacyFull($settings); $settings->getLogger()->setLevel(Logger::ULTRA_VERBOSE); $settings->getAuth()->setPfs(true); $MadelineProto = new API($session, $settings); $MadelineProto->help->getConfig(); $MadelineProto->logger('About to import auth!', Logger::FATAL_ERROR); $MadelineProto->importAuthorization($authorization, $main_dc_id); return $MadelineProto; } public static function telethon(string $session, string $new_session, $settings = []) { if (!\extension_loaded('sqlite3')) { throw new Exception(['extension', 'sqlite3']); } if (!isset(\pathinfo($session)['extension'])) { $session .= '.session'; } $session = Tools::absolute($session); $sqlite = new PDO("sqlite:$session"); $sqlite->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $sessions = $sqlite->query('SELECT * FROM sessions')->fetchAll(); $dcs = []; foreach ($sessions as $dc) { $dcs[$dc['dc_id']] = $dc['auth_key']; } return self::importAuthorization($dcs, $dc['dc_id'], $new_session, $settings); } public static function pyrogram(string $session, string $new_session, $settings = []) { \set_error_handler(['\\danog\\MadelineProto\\Exception', 'ExceptionErrorHandler']); if (!isset(\pathinfo($session)['extension'])) { $session .= '.session'; } $session = Tools::absolute($session); $session = \json_decode(\file_get_contents($session), true); $session['auth_key'] = \base64_decode(\implode('', $session['auth_key'])); Assert::notFalse($session['auth_key']); Assert::integer($session['dc_id']); $settings['connection_settings']['all']['test_mode'] = $session['test_mode']; return self::importAuthorization([$session['dc_id'] => $session['auth_key']], $session['dc_id'], $new_session, $settings); } public static function zerobias($session, $new_session, $settings = []) { \set_error_handler(['\\danog\\MadelineProto\\Exception', 'ExceptionErrorHandler']); if (\is_string($session)) { $session = \json_decode($session, true); } $dc = $session['dc']; $key = \hex2bin($session["dc$dc".'_auth_key']); Assert::integer($dc); Assert::string($key); return self::importAuthorization([$dc => $key], $dc, $new_session, $settings); } public static function tdesktop_md5($data) { $result = ''; foreach (\str_split(\md5($data), 2) as $byte) { $result .= \strrev($byte); } return \strtoupper($result); } const FILEOPTION_SAFE = 1; const FILEOPTION_USER = 2; public static $tdesktop_base_path; public static $tdesktop_user_base_path; public static $tdesktop_key; public static function tdesktop_fopen($fileName, $options = self::FILEOPTION_SAFE|self::FILEOPTION_USER) { $name = ($options & self::FILEOPTION_USER ? self::$tdesktop_user_base_path : self::$tdesktop_base_path).$fileName; $totry = []; foreach (['0', '1', 's'] as $x) { if (\file_exists($name.$x)) { $totry[] = \fopen($name.$x, 'rb'); } } foreach ($totry as $fp) { if (\stream_get_contents($fp, 4) !== 'TDF$') { Logger::log('Wrong magic', Logger::ERROR); continue; } $versionBytes = \stream_get_contents($fp, 4); $version = Tools::unpackSignedInt($versionBytes); Logger::log("TDesktop version: $version"); $data = \stream_get_contents($fp); $md5 = \substr($data, -16); $data = \substr($data, 0, -16); $length = \pack('l', \strlen($data)); $length = Magic::$BIG_ENDIAN ? \strrev($length) : $length; if (\md5($data.$length.$versionBytes.'TDF$', true) !== $md5) { Logger::log('Wrong MD5', Logger::ERROR); } $res = \fopen('php://memory', 'rw+b'); \fwrite($res, $data); \fseek($res, 0); return $res; } throw new Exception("Could not open $fileName"); } public static function tdesktop_fopen_encrypted($fileName, $options = 3) { $f = self::tdesktop_fopen($fileName, $options); $data = self::tdesktop_read_bytearray($f); $res = self::tdesktop_decrypt($data, self::$tdesktop_key); $length = \unpack('V', \stream_get_contents($res, 4))[1]; if ($length > \fstat($res)['size'] || $length < 4) { throw new Exception('Wrong length'); } return $res; } public static function tdesktop_read_bytearray($fp, bool $asString = false) { $length = Tools::unpackSignedInt(\strrev(\stream_get_contents($fp, 4))); $data = $length > 0 ? \stream_get_contents($fp, $length) : ''; if ($asString) { return $data; } $res = \fopen('php://memory', 'rw+b'); \fwrite($res, $data); \fseek($res, 0); return $res; } public static function tdesktop_decrypt($data, $auth_key) { $message_key = \stream_get_contents($data, 16); $encrypted_data = \stream_get_contents($data); [$aes_key, $aes_iv] = Crypt::oldAesCalculate($message_key, $auth_key, false); $decrypted_data = Crypt::igeDecrypt($encrypted_data, $aes_key, $aes_iv); if ($message_key != \substr(\sha1($decrypted_data, true), 0, 16)) { throw new SecurityException('msg_key mismatch'); } $res = \fopen('php://memory', 'rw+b'); \fwrite($res, $decrypted_data); \fseek($res, 0); return $res; } const dbiKey = 0x00; const dbiUser = 0x01; const dbiDcOptionOldOld = 0x02; const dbiChatSizeMax = 0x03; const dbiMutePeer = 0x04; const dbiSendKey = 0x05; const dbiAutoStart = 0x06; const dbiStartMinimized = 0x07; const dbiSoundNotify = 0x08; const dbiWorkMode = 0x09; const dbiSeenTrayTooltip = 0x0a; const dbiDesktopNotify = 0x0b; const dbiAutoUpdate = 0x0c; const dbiLastUpdateCheck = 0x0d; const dbiWindowPosition = 0x0e; const dbiConnectionTypeOld = 0x0f; // 0x10 reserved const dbiDefaultAttach = 0x11; const dbiCatsAndDogs = 0x12; const dbiReplaceEmojis = 0x13; const dbiAskDownloadPath = 0x14; const dbiDownloadPathOld = 0x15; const dbiScale = 0x16; const dbiEmojiTabOld = 0x17; const dbiRecentEmojiOldOld = 0x18; const dbiLoggedPhoneNumber = 0x19; const dbiMutedPeers = 0x1a; // 0x1b reserved const dbiNotifyView = 0x1c; const dbiSendToMenu = 0x1d; const dbiCompressPastedImage = 0x1e; const dbiLangOld = 0x1f; const dbiLangFileOld = 0x20; const dbiTileBackground = 0x21; const dbiAutoLock = 0x22; const dbiDialogLastPath = 0x23; const dbiRecentEmojiOld = 0x24; const dbiEmojiVariantsOld = 0x25; const dbiRecentStickers = 0x26; const dbiDcOptionOld = 0x27; const dbiTryIPv6 = 0x28; const dbiSongVolume = 0x29; const dbiWindowsNotificationsOld = 0x30; const dbiIncludeMuted = 0x31; const dbiMegagroupSizeMax = 0x32; const dbiDownloadPath = 0x33; const dbiAutoDownload = 0x34; const dbiSavedGifsLimit = 0x35; const dbiShowingSavedGifsOld = 0x36; const dbiAutoPlay = 0x37; const dbiAdaptiveForWide = 0x38; const dbiHiddenPinnedMessages = 0x39; const dbiRecentEmoji = 0x3a; const dbiEmojiVariants = 0x3b; const dbiDialogsMode = 0x40; const dbiModerateMode = 0x41; const dbiVideoVolume = 0x42; const dbiStickersRecentLimit = 0x43; const dbiNativeNotifications = 0x44; const dbiNotificationsCount = 0x45; const dbiNotificationsCorner = 0x46; const dbiThemeKey = 0x47; const dbiDialogsWidthRatioOld = 0x48; const dbiUseExternalVideoPlayer = 0x49; const dbiDcOptions = 0x4a; const dbiMtpAuthorization = 0x4b; const dbiLastSeenWarningSeenOld = 0x4c; const dbiAuthSessionSettings = 0x4d; const dbiLangPackKey = 0x4e; const dbiConnectionType = 0x4f; const dbiStickersFavedLimit = 0x50; const dbiSuggestStickersByEmoji = 0x51; const dbiEncryptedWithSalt = 333; const dbiEncrypted = 444; // 500-600 reserved const dbiVersion = 666; public static function tdesktop(string $session, string $new_session, $settings = []) { \set_error_handler(['\\danog\\MadelineProto\\Exception', 'ExceptionErrorHandler']); $settings['old_session_key'] ??= 'data'; $settings['old_session_passcode'] ??= ''; if (\basename($session) !== 'tdata') { $session .= DIRECTORY_SEPARATOR.'tdata'; } [$part_one_md5, $part_two_md5] = \str_split(self::tdesktop_md5($settings['old_session_key']), 16); self::$tdesktop_base_path = $session.DIRECTORY_SEPARATOR; self::$tdesktop_user_base_path = self::$tdesktop_base_path.$part_one_md5.DIRECTORY_SEPARATOR; $data = self::tdesktop_fopen('map'); $salt = self::tdesktop_read_bytearray($data, true); $encryptedKey = self::tdesktop_read_bytearray($data); if (\strlen($salt)) { $keyIterCount = \strlen($settings['old_session_passcode']) ? 4000 : 4; $passKey = \openssl_pbkdf2($settings['old_session_passcode'], $salt, 256, $keyIterCount); self::$tdesktop_key = \stream_get_contents( self::tdesktop_read_bytearray( self::tdesktop_decrypt($encryptedKey, $passKey), ), ); } else { $key = 'key_'.$settings['old_session_key']; $data = self::tdesktop_fopen($key, self::FILEOPTION_SAFE); $salt = self::tdesktop_read_bytearray($data, true); if (\strlen($salt) !== 32) { throw new Exception('Length of salt is wrong!'); } $encryptedKey = self::tdesktop_read_bytearray($data); $encryptedInfo = self::tdesktop_read_bytearray($data); $hash = \hash('sha512', $salt.$settings['old_session_passcode'].$salt, true); $iterCount = \strlen($settings['old_session_passcode']) ? 100000 : 1; $passKey = \openssl_pbkdf2($hash, $salt, 256, $iterCount, 'sha512'); $key = self::tdesktop_read_bytearray(self::tdesktop_decrypt($encryptedKey, $passKey), true); $info = self::tdesktop_read_bytearray(self::tdesktop_decrypt($encryptedInfo, $key)); self::$tdesktop_key = $key; $count = Tools::unpackSignedInt(\strrev(\stream_get_contents($info, 4))); Logger::log("Number of accounts: $count"); for ($i = 0; $i != $count; $i++) { $idx = Tools::unpackSignedInt(\strrev(\stream_get_contents($info, 4))); if ($idx >= 0) { $dataName = $settings['old_session_key']; if ($idx > 0) { $dataName = $settings['old_session_key'].'#'.($idx+1); } [$part_one_md5] = \str_split(self::tdesktop_md5($dataName), 16); } } } $main = self::tdesktop_fopen_encrypted($part_one_md5, self::FILEOPTION_SAFE); $auth_keys = []; while (!\feof($main)) { $magic = Tools::unpackSignedInt(\strrev(\stream_get_contents($main, 4))); switch ($magic) { case self::dbiDcOptionOldOld: \stream_get_contents($main, 4); self::tdesktop_read_bytearray($main); self::tdesktop_read_bytearray($main); \stream_get_contents($main, 4); break; case self::dbiDcOptionOld: \stream_get_contents($main, 8); self::tdesktop_read_bytearray($main); \stream_get_contents($main, 4); break; case self::dbiDcOptions: self::tdesktop_read_bytearray($main); break; case self::dbiUser: \stream_get_contents($main, 4); $main_dc_id = Tools::unpackSignedInt(\strrev(\stream_get_contents($main, 4))); break; case self::dbiKey: $auth_keys[Tools::unpackSignedInt(\strrev(\stream_get_contents($main, 4)))] = \stream_get_contents($main, 256); break; case self::dbiMtpAuthorization: $main = self::tdesktop_read_bytearray($main); //stream_get_contents($main, 4); $user_id = Tools::unpackSignedInt(\strrev(\stream_get_contents($main, 4))); $main_dc_id = Tools::unpackSignedInt(\strrev(\stream_get_contents($main, 4))); $length = Tools::unpackSignedInt(\strrev(\stream_get_contents($main, 4))); for ($x = 0; $x < $length; $x++) { $dc = Tools::unpackSignedInt(\strrev(\stream_get_contents($main, 4))); $auth_key = \stream_get_contents($main, 256); if ($dc <= 5) { $auth_keys[$dc] = $auth_key; } } break 2; case self::dbiAutoDownload: \stream_get_contents($main, 12); break; case self::dbiDialogsMode: \stream_get_contents($main, 8); break; case self::dbiAuthSessionSettings: self::tdesktop_read_bytearray($main); break; case self::dbiConnectionTypeOld: switch (Tools::unpackSignedInt(\strrev(\stream_get_contents($main, 4)))) { case 2: case 3: self::tdesktop_read_bytearray($main); \stream_get_contents($main, 4); self::tdesktop_read_bytearray($main); self::tdesktop_read_bytearray($main); break; } break; case self::dbiConnectionType: \stream_get_contents($main, 8); self::tdesktop_read_bytearray($main); \stream_get_contents($main, 4); self::tdesktop_read_bytearray($main); self::tdesktop_read_bytearray($main); break; case self::dbiThemeKey: case self::dbiLangPackKey: case self::dbiMutePeer: \stream_get_contents($main, 8); break; case self::dbiWindowPosition: \stream_get_contents($main, 24); break; case self::dbiLoggedPhoneNumber: self::tdesktop_read_bytearray($main); break; case self::dbiMutedPeers: $length = Tools::unpackSignedInt(\strrev(\stream_get_contents($main, 4))); for ($x = 0; $x < $length; $x++) { \stream_get_contents($main, 8); } // no break case self::dbiDownloadPathOld: self::tdesktop_read_bytearray($main); break; case self::dbiDialogLastPath: self::tdesktop_read_bytearray($main); break; case self::dbiDownloadPath: self::tdesktop_read_bytearray($main); self::tdesktop_read_bytearray($main); break; default: throw new \Exception("Unknown type $magic"); break; } } return self::importAuthorization($auth_keys, $main_dc_id, $new_session, $settings); } }