· 7 years ago · Dec 27, 2018, 04:02 AM
1<?php
2
3
4class Authenticator
5{
6 protected $length = 6;
7 public function generateRandomSecret($secretLength = 16)
8 {
9 $secret = '';
10 $validChars = array(
11 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
12 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
13 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
14 'Y', 'Z', '2', '3', '4', '5', '6', '7',
15 '=',
16 );
17
18 // Valid secret lengths are 80 to 640 bits
19 if ($secretLength < 16 || $secretLength > 128) {
20 throw new Exception('Bad secret length');
21 }
22 $random = false;
23 if (function_exists('random_bytes')) {
24 $random = random_bytes($secretLength);
25 } elseif (function_exists('mcrypt_create_iv')) {
26 $random = mcrypt_create_iv($secretLength, MCRYPT_DEV_URANDOM);
27 } elseif (function_exists('openssl_random_pseudo_bytes')) {
28 $random = openssl_random_pseudo_bytes($secretLength, $cryptoStrong);
29 if (!$cryptoStrong) {
30 $random = false;
31 }
32 }
33 if ($random !== false) {
34 for ($i = 0; $i < $secretLength; ++$i) {
35 $secret .= $validChars[ord($random[$i]) & 31];
36 }
37 } else {
38 throw new Exception('Cannot create secure random secret due to source unavailbility');
39 }
40
41 return $secret;
42 }
43
44
45 public function getCode($secret, $timeSlice = null)
46 {
47 if ($timeSlice === null) {
48 $timeSlice = floor(time() / 30);
49 }
50
51 $secretkey = $this->debase32($secret);
52
53 $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
54 $hm = hash_hmac('SHA1', $time, $secretkey, true);
55 $offset = ord(substr($hm, -1)) & 0x0F;
56 $hashpart = substr($hm, $offset, 4);
57
58 $value = unpack('N', $hashpart);
59 $value = $value[1];
60 $value = $value & 0x7FFFFFFF;
61
62 $modulo = pow(10, $this->length);
63
64 return str_pad($value % $modulo, $this->length, '0', STR_PAD_LEFT);
65 }
66
67
68 public function getQR($name, $secret, $title = null, $params = array())
69 {
70 $width = !empty($params['width']) && (int) $params['width'] > 0 ? (int) $params['width'] : 200;
71 $height = !empty($params['height']) && (int) $params['height'] > 0 ? (int) $params['height'] : 200;
72 $level = !empty($params['level']) && array_search($params['level'], array('L', 'M', 'Q', 'H')) !== false ? $params['level'] : 'M';
73
74 $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
75 if (isset($title)) {
76 $urlencoded .= urlencode('&issuer='.urlencode($title));
77 }
78
79 return 'https://chart.googleapis.com/chart?chs='.$width.'x'.$height.'&chld='.$level.'|0&cht=qr&chl='.$urlencoded.'';
80 }
81
82 public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null)
83 {
84 if ($currentTimeSlice === null) {
85 $currentTimeSlice = floor(time() / 30);
86 }
87
88 if (strlen($code) != 6) {
89 return false;
90 }
91
92 for ($i = -$discrepancy; $i <= $discrepancy; ++$i) {
93 $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
94 if ($this->timingSafeEquals($calculatedCode, $code)) {
95 return true;
96 }
97 }
98
99 return false;
100 }
101
102
103 public function setCodeLength($length)
104 {
105 $this->length = $length;
106
107 return $this;
108 }
109
110
111 protected function debase32($secret)
112 {
113 if (empty($secret)) {
114 return '';
115 }
116
117 $base32chars = array(
118 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
119 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
120 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
121 'Y', 'Z', '2', '3', '4', '5', '6', '7',
122 '=',
123 );
124 $base32charsFlipped = array_flip($base32chars);
125
126 $paddingCharCount = substr_count($secret, $base32chars[32]);
127 $allowedValues = array(6, 4, 3, 1, 0);
128 if (!in_array($paddingCharCount, $allowedValues)) {
129 return false;
130 }
131 for ($i = 0; $i < 4; ++$i) {
132 if ($paddingCharCount == $allowedValues[$i] &&
133 substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) {
134 return false;
135 }
136 }
137 $secret = str_replace('=', '', $secret);
138 $secret = str_split($secret);
139 $binaryString = '';
140 for ($i = 0; $i < count($secret); $i = $i + 8) {
141 $x = '';
142 if (!in_array($secret[$i], $base32chars)) {
143 return false;
144 }
145 for ($j = 0; $j < 8; ++$j) {
146 $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
147 }
148 $eightBits = str_split($x, 8);
149 for ($z = 0; $z < count($eightBits); ++$z) {
150 $binaryString .= (($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : '';
151 }
152 }
153
154 return $binaryString;
155 }
156
157
158 private function timingSafeEquals($safeString, $userString)
159 {
160 if (function_exists('hash_equals')) {
161 return hash_equals($safeString, $userString);
162 }
163 $safeLen = strlen($safeString);
164 $userLen = strlen($userString);
165
166 if ($userLen != $safeLen) {
167 return false;
168 }
169
170 $result = 0;
171
172 for ($i = 0; $i < $userLen; ++$i) {
173 $result |= (ord($safeString[$i]) ^ ord($userString[$i]));
174 }
175 return $result === 0;
176 }
177}