* @copyright 2015 Smiley * @license MIT */ declare(strict_types=1); namespace chillerlan\Authenticator; use chillerlan\Authenticator\Authenticators\AuthenticatorInterface; use chillerlan\Settings\SettingsContainerInterface; use InvalidArgumentException; use SensitiveParameter; use function http_build_query; use function rawurlencode; use function sprintf; use function trim; use const PHP_QUERY_RFC3986; /** * Yet another Google authenticator implementation! * * @link https://tools.ietf.org/html/rfc4226 * @link https://tools.ietf.org/html/rfc6238 * @link https://github.com/google/google-authenticator * @link https://openauthentication.org/specifications-technical-resources/ * @link https://blog.ircmaxell.com/2014/11/its-all-about-time.html */ class Authenticator{ protected SettingsContainerInterface|AuthenticatorOptions $options; protected AuthenticatorInterface $authenticator; protected string $mode = AuthenticatorInterface::TOTP; /** * Authenticator constructor */ public function __construct( SettingsContainerInterface|AuthenticatorOptions $options = new AuthenticatorOptions, string|null $secret = null ){ // phpcs:ignore $this->setOptions($options); if($secret !== null){ $this->setSecret($secret); } } /** * Sets an options instance and invokes an authenticator according to the given mode * * Please note that this will reset the secret phrase stored with the authenticator instance * if a different mode than the current is given. */ public function setOptions(SettingsContainerInterface|AuthenticatorOptions $options):self{ $this->options = $options; // invoke a new authenticator interface if necessary if(!isset($this->authenticator) || $this->options->mode !== $this->mode){ $this->mode = $this->options->mode; $this->authenticator = new (AuthenticatorInterface::MODES[$this->options->mode])($this->options); } $this->authenticator->setOptions($this->options); return $this; } /** * Sets a secret phrase from a Base32 representation * * @codeCoverageIgnore */ public function setSecret(#[SensitiveParameter] string $encodedSecret):self{ $this->authenticator->setSecret($encodedSecret); return $this; } /** * Returns a Base32 representation of the current secret phrase * * @codeCoverageIgnore */ public function getSecret():string{ return $this->authenticator->getSecret(); } /** * Generates a new (secure random) secret phrase * * @codeCoverageIgnore */ public function createSecret(int|null $length = null):string{ return $this->authenticator->createSecret($length); } /** * Creates a new OTP code with the given secret * * $data may be * - a UNIX timestamp (TOTP) * - a counter value (HOTP) * * @codeCoverageIgnore */ public function code(int|null $data = null):string{ return $this->authenticator->code($data); } /** * Checks the given $code against the secret * * $data may be * - a UNIX timestamp (TOTP) * - a counter value (HOTP) * * @codeCoverageIgnore */ public function verify(#[SensitiveParameter] string $otp, int|null $data = null):bool{ return $this->authenticator->verify($otp, $data); } /** * Creates a URI for use in QR codes for example * * @link https://github.com/google/google-authenticator/wiki/Key-Uri-Format#parameters * * @throws \InvalidArgumentException */ public function getUri(string $label, string $issuer, int|null $hotpCounter = null, bool|null $omitSettings = null):string{ $label = trim($label); $issuer = trim($issuer); if(empty($label) || empty($issuer)){ throw new InvalidArgumentException('$label and $issuer cannot be empty'); } $values = [ 'secret' => $this->authenticator->getSecret(), 'issuer' => $issuer, ]; if($omitSettings !== true){ $values['digits'] = $this->options->digits; $values['algorithm'] = $this->options->algorithm; if($this->mode === AuthenticatorInterface::TOTP){ $values['period'] = $this->options->period; } if($this->mode === AuthenticatorInterface::HOTP && $hotpCounter !== null){ $values['counter'] = $hotpCounter; } } $values = http_build_query($values, '', '&', PHP_QUERY_RFC3986); return sprintf('otpauth://%s/%s?%s', $this->mode, rawurlencode($label), $values); } }