altocompta/vendor/chillerlan/php-authenticator/src/Authenticators/SteamGuard.php

162 lines
4 KiB
PHP

<?php
/**
* Class SteamGuard
*
* @created 20.06.2023
* @author smiley <smiley@chillerlan.net>
* @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);
}
}