1 <?php 2 3 declare(strict_types=1); 4 5 /* 6 * The MIT License (MIT) 7 * 8 * Copyright (c) 2014-2018 Spomky-Labs 9 * 10 * This software may be modified and distributed under the terms 11 * of the MIT license. See the LICENSE file for details. 12 */ 13 14 namespace OTPHP; 15 16 use Assert\Assertion; 17 use ParagonIE\ConstantTime\Base32; 18 19 abstract class OTP implements OTPInterface 20 { 21 use ParameterTrait; 22 23 /** 24 * OTP constructor. 25 * 26 * @param string|null $secret 27 * @param string $digest 28 * @param int $digits 29 */ 30 protected function __construct($secret, string $digest, int $digits) 31 { 32 $this->setSecret($secret); 33 $this->setDigest($digest); 34 $this->setDigits($digits); 35 } 36 37 /** 38 * {@inheritdoc} 39 */ 40 public function getQrCodeUri(string $uri = 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl={PROVISIONING_URI}', string $placeholder = '{PROVISIONING_URI}'): string 41 { 42 $provisioning_uri = urlencode($this->getProvisioningUri()); 43 44 return str_replace($placeholder, $provisioning_uri, $uri); 45 } 46 47 /** 48 * @param int $input 49 * 50 * @return string The OTP at the specified input 51 */ 52 protected function generateOTP(int $input): string 53 { 54 $hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret()); 55 $hmac = []; 56 foreach (str_split($hash, 2) as $hex) { 57 $hmac[] = hexdec($hex); 58 } 59 $offset = $hmac[count($hmac) - 1] & 0xF; 60 $code = ($hmac[$offset + 0] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF); 61 $otp = $code % pow(10, $this->getDigits()); 62 63 return str_pad((string) $otp, $this->getDigits(), '0', STR_PAD_LEFT); 64 } 65 66 /** 67 * {@inheritdoc} 68 */ 69 public function at(int $timestamp): string 70 { 71 return $this->generateOTP($timestamp); 72 } 73 74 /** 75 * @param array $options 76 */ 77 protected function filterOptions(array &$options) 78 { 79 foreach (['algorithm' => 'sha1', 'period' => 30, 'digits' => 6] as $key => $default) { 80 if (isset($options[$key]) && $default === $options[$key]) { 81 unset($options[$key]); 82 } 83 } 84 85 ksort($options); 86 } 87 88 /** 89 * @param string $type 90 * @param array $options 91 * 92 * @return string 93 */ 94 protected function generateURI(string $type, array $options): string 95 { 96 $label = $this->getLabel(); 97 Assertion::string($label, 'The label is not set.'); 98 Assertion::false($this->hasColon($label), 'Label must not contain a colon.'); 99 $options = array_merge($options, $this->getParameters()); 100 $this->filterOptions($options); 101 $params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&')); 102 103 return sprintf('otpauth://%s/%s?%s', $type, rawurlencode((null !== $this->getIssuer() ? $this->getIssuer().':' : '').$label), $params); 104 } 105 106 /** 107 * @return string 108 */ 109 private function getDecodedSecret(): string 110 { 111 try { 112 $secret = Base32::decodeUpper($this->getSecret()); 113 } catch (\Exception $e) { 114 throw new \RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?'); 115 } 116 117 return $secret; 118 } 119 120 /** 121 * @param int $int 122 * 123 * @return string 124 */ 125 private function intToByteString(int $int): string 126 { 127 $result = []; 128 while (0 !== $int) { 129 $result[] = chr($int & 0xFF); 130 $int >>= 8; 131 } 132 133 return str_pad(implode(array_reverse($result)), 8, "\000", STR_PAD_LEFT); 134 } 135 136 /** 137 * @param string $safe 138 * @param string $user 139 * 140 * @return bool 141 */ 142 protected function compareOTP(string $safe, string $user): bool 143 { 144 return hash_equals($safe, $user); 145 } 146 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body