* @copyright 2023 smiley * @license MIT * * @noinspection PhpComposerExtensionStubsInspection */ declare(strict_types=1); namespace chillerlan\Authenticator\Authenticators; use chillerlan\Authenticator\Common\Base64; use RuntimeException; use SensitiveParameter; use function curl_close; use function curl_exec; use function curl_getinfo; use function curl_init; use function curl_setopt_array; use function floor; use function intdiv; use function is_array; use function json_decode; use function sprintf; use function time; use const CURLOPT_HTTPHEADER; use const CURLOPT_POST; use const CURLOPT_POSTFIELDS; use const CURLOPT_RETURNTRANSFER; /** * /data/data/com.valvesoftware.android.steam.community/f/Steamguard-STEAMID64 * * @see https://help.steampowered.com/en/faqs/view/7EFD-3CAE-64D3-1C31 * @see https://github.com/SoftCreatR/php-steam-guard */ final class SteamGuard extends TOTP{ private const steamCodeChars = '23456789BCDFGHJKMNPQRTVWXY'; private const steamTimeURL = 'https://api.steampowered.com/ITwoFactorService/QueryTime/v0001'; /** * @inheritDoc */ public function setSecret(#[SensitiveParameter] string $encodedSecret):AuthenticatorInterface{ $this->secret = Base64::decode($this->checkEncodedSecret($encodedSecret)); return $this; } /** * @inheritDoc */ public function getSecret():string{ if($this->secret === null){ throw new RuntimeException('No secret set'); } return Base64::encode($this->secret); } /** * @inheritDoc * @codeCoverageIgnore */ public function createSecret(int|null $length = null):string{ throw new RuntimeException('Not implemented'); } /** * @inheritDoc */ public function getCounter(int|null $data = null):int{ // the period is fixed to 30 seconds for Steam Guard $this->options->period = 30; return parent::getCounter($data); } /** * @inheritDoc */ public function getHMAC(int $counter):string{ // algorithm is fixed to sha1 for Steam Guard $this->options->algorithm = self::ALGO_SHA1; return parent::getHMAC($counter); } /** * @inheritDoc */ public function getOTP(#[SensitiveParameter] int $code):string{ $str = ''; $len = 26; // strlen($this::steamCodeChars) // length is fixed to 5 for Steam for($i = 0; $i < 5; $i++){ $str .= $this::steamCodeChars[($code % $len)]; $code = intdiv($code, $len); } return $str; } /** * @inheritDoc * @throws \RuntimeException */ public function getServerTime():int{ if($this->options->forceTimeRefresh === false && $this->serverTime !== 0){ return $this->getAdjustedTime($this->serverTime, $this->lastRequestTime); } $this->serverTime = 0; $this->lastRequestTime = 0; $options = [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => 'steamid=0', CURLOPT_HTTPHEADER => [sprintf('User-Agent: %s', $this::userAgent)], ]; $ch = curl_init($this::steamTimeURL); // it's almost impossible to run into this, but hey, phpstan happy if($ch === false){ throw new RuntimeException('curl_init error'); // @codeCoverageIgnore } curl_setopt_array($ch, $options); $response = curl_exec($ch); $info = curl_getinfo($ch); curl_close($ch); if($info['http_code'] !== 200 || $response === false){ // I'm not going to investigate the error further as this shouldn't happen usually throw new RuntimeException(sprintf('Steam API request error: HTTP/%s', $info['http_code'])); // @codeCoverageIgnore } $json = json_decode($response, true); if(!is_array($json) || !isset($json['response']['server_time'])){ throw new RuntimeException('Unable to decode Steam API response'); // @codeCoverageIgnore } $this->serverTime = (int)$json['response']['server_time']; $this->lastRequestTime = (time() - (int)floor($info['total_time'])); return $this->getAdjustedTime($this->serverTime, $this->lastRequestTime); } }