├── LICENSE ├── README ├── example.php ├── lib ├── FixedByteNotation.php └── GoogleAuthenticator.php ├── tmpl ├── ask-for-otp.php ├── loggedin.php ├── login-error.php ├── login.php └── show-qr.php ├── users.dat └── web ├── Users.php └── index.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Liip AG 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Ported from http://code.google.com/p/google-authenticator/ 2 | 3 | You can use the Google Authenticator app from here 4 | http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=1066447 5 | to generate One Time Passwords/Tokens and check them with this little 6 | PHP app (Of course, you can also create them with this). 7 | 8 | There are many real world applications for that, but noone implemented it yet. 9 | 10 | See example.php for how to use it. 11 | 12 | There's a little web app showing how it works in web/, please make users.dat 13 | writeable for the webserver, doesn't really work otherwise (it can't save the 14 | secret). Try to login with chregu/foobar. 15 | 16 | 17 | What's missing in the example: 18 | *** 19 | 20 | * Prevent replay attacks. One token should only be used once 21 | * Show QR Code only when providing password again (or not at all) 22 | * Regenrate secret -------------------------------------------------------------------------------- /example.php: -------------------------------------------------------------------------------- 1 | getCode($secret); 12 | 13 | print "\n"; 14 | 15 | print "Check if $code is valid: "; 16 | 17 | if ($g->checkCode($secret,$code)) { 18 | print "YES \n"; 19 | } else { 20 | print "NO \n"; 21 | } 22 | 23 | $secret = $g->generateSecret(); 24 | print "Get a new Secret: $secret \n"; 25 | 26 | print "The QR Code for this secret (to scan with the Google Authenticator App: \n"; 27 | print $g->getURL('chregu','example.org',$secret); 28 | print "\n"; -------------------------------------------------------------------------------- /lib/FixedByteNotation.php: -------------------------------------------------------------------------------- 1 | = ($radix <<= 1) && $bitsPerCharacter < 8) { 62 | $bitsPerCharacter++; 63 | } 64 | 65 | $radix >>= 1; 66 | 67 | } elseif ($bitsPerCharacter > 8) { 68 | // $bitsPerCharacter must not be greater than 8 69 | $bitsPerCharacter = 8; 70 | $radix = 256; 71 | 72 | } else { 73 | $radix = 1 << $bitsPerCharacter; 74 | } 75 | 76 | $this->_chars = $chars; 77 | $this->_bitsPerCharacter = $bitsPerCharacter; 78 | $this->_radix = $radix; 79 | $this->_rightPadFinalBits = $rightPadFinalBits; 80 | $this->_padFinalGroup = $padFinalGroup; 81 | $this->_padCharacter = $padCharacter[0]; 82 | } 83 | 84 | /** 85 | * Encode a string 86 | * 87 | * @param string $rawString Binary data to encode 88 | * @return string 89 | */ 90 | public function encode($rawString) 91 | { 92 | // Unpack string into an array of bytes 93 | $bytes = unpack('C*', $rawString); 94 | $byteCount = count($bytes); 95 | 96 | $encodedString = ''; 97 | $byte = array_shift($bytes); 98 | $bitsRead = 0; 99 | 100 | $chars = $this->_chars; 101 | $bitsPerCharacter = $this->_bitsPerCharacter; 102 | $rightPadFinalBits = $this->_rightPadFinalBits; 103 | $padFinalGroup = $this->_padFinalGroup; 104 | $padCharacter = $this->_padCharacter; 105 | 106 | // Generate encoded output; 107 | // each loop produces one encoded character 108 | for ($c = 0; $c < $byteCount * 8 / $bitsPerCharacter; $c++) { 109 | 110 | // Get the bits needed for this encoded character 111 | if ($bitsRead + $bitsPerCharacter > 8) { 112 | // Not enough bits remain in this byte for the current 113 | // character 114 | // Save the remaining bits before getting the next byte 115 | $oldBitCount = 8 - $bitsRead; 116 | $oldBits = $byte ^ ($byte >> $oldBitCount << $oldBitCount); 117 | $newBitCount = $bitsPerCharacter - $oldBitCount; 118 | 119 | if (!$bytes) { 120 | // Last bits; match final character and exit loop 121 | if ($rightPadFinalBits) $oldBits <<= $newBitCount; 122 | $encodedString .= $chars[$oldBits]; 123 | 124 | if ($padFinalGroup) { 125 | // Array of the lowest common multiples of 126 | // $bitsPerCharacter and 8, divided by 8 127 | $lcmMap = array(1 => 1, 2 => 1, 3 => 3, 4 => 1, 128 | 5 => 5, 6 => 3, 7 => 7, 8 => 1); 129 | $bytesPerGroup = $lcmMap[$bitsPerCharacter]; 130 | $pads = $bytesPerGroup * 8 / $bitsPerCharacter 131 | - ceil((strlen($rawString) % $bytesPerGroup) 132 | * 8 / $bitsPerCharacter); 133 | $encodedString .= str_repeat($padCharacter[0], $pads); 134 | } 135 | 136 | break; 137 | } 138 | 139 | // Get next byte 140 | $byte = array_shift($bytes); 141 | $bitsRead = 0; 142 | 143 | } else { 144 | $oldBitCount = 0; 145 | $newBitCount = $bitsPerCharacter; 146 | } 147 | 148 | // Read only the needed bits from this byte 149 | $bits = $byte >> 8 - ($bitsRead + ($newBitCount)); 150 | $bits ^= $bits >> $newBitCount << $newBitCount; 151 | $bitsRead += $newBitCount; 152 | 153 | if ($oldBitCount) { 154 | // Bits come from seperate bytes, add $oldBits to $bits 155 | $bits = ($oldBits << $newBitCount) | $bits; 156 | } 157 | 158 | $encodedString .= $chars[$bits]; 159 | } 160 | 161 | return $encodedString; 162 | } 163 | 164 | /** 165 | * Decode a string 166 | * 167 | * @param string $encodedString Data to decode 168 | * @param boolean $caseSensitive 169 | * @param boolean $strict Returns NULL if $encodedString contains 170 | * an undecodable character 171 | * @return string|NULL 172 | */ 173 | public function decode($encodedString, $caseSensitive = TRUE, 174 | $strict = FALSE) 175 | { 176 | if (!$encodedString || !is_string($encodedString)) { 177 | // Empty string, nothing to decode 178 | return ''; 179 | } 180 | 181 | $chars = $this->_chars; 182 | $bitsPerCharacter = $this->_bitsPerCharacter; 183 | $radix = $this->_radix; 184 | $rightPadFinalBits = $this->_rightPadFinalBits; 185 | $padFinalGroup = $this->_padFinalGroup; 186 | $padCharacter = $this->_padCharacter; 187 | 188 | // Get index of encoded characters 189 | if ($this->_charmap) { 190 | $charmap = $this->_charmap; 191 | 192 | } else { 193 | $charmap = array(); 194 | 195 | for ($i = 0; $i < $radix; $i++) { 196 | $charmap[$chars[$i]] = $i; 197 | } 198 | 199 | $this->_charmap = $charmap; 200 | } 201 | 202 | // The last encoded character is $encodedString[$lastNotatedIndex] 203 | $lastNotatedIndex = strlen($encodedString) - 1; 204 | 205 | // Remove trailing padding characters 206 | while ($encodedString[$lastNotatedIndex] == $padCharacter[0]) { 207 | $encodedString = substr($encodedString, 0, $lastNotatedIndex); 208 | $lastNotatedIndex--; 209 | } 210 | 211 | $rawString = ''; 212 | $byte = 0; 213 | $bitsWritten = 0; 214 | 215 | // Convert each encoded character to a series of unencoded bits 216 | for ($c = 0; $c <= $lastNotatedIndex; $c++) { 217 | 218 | if (!isset($charmap[$encodedString[$c]]) && !$caseSensitive) { 219 | // Encoded character was not found; try other case 220 | if (isset($charmap[$cUpper 221 | = strtoupper($encodedString[$c])])) { 222 | $charmap[$encodedString[$c]] = $charmap[$cUpper]; 223 | 224 | } elseif (isset($charmap[$cLower 225 | = strtolower($encodedString[$c])])) { 226 | $charmap[$encodedString[$c]] = $charmap[$cLower]; 227 | } 228 | } 229 | 230 | if (isset($charmap[$encodedString[$c]])) { 231 | $bitsNeeded = 8 - $bitsWritten; 232 | $unusedBitCount = $bitsPerCharacter - $bitsNeeded; 233 | 234 | // Get the new bits ready 235 | if ($bitsNeeded > $bitsPerCharacter) { 236 | // New bits aren't enough to complete a byte; shift them 237 | // left into position 238 | $newBits = $charmap[$encodedString[$c]] << $bitsNeeded 239 | - $bitsPerCharacter; 240 | $bitsWritten += $bitsPerCharacter; 241 | 242 | } elseif ($c != $lastNotatedIndex || $rightPadFinalBits) { 243 | // Zero or more too many bits to complete a byte; 244 | // shift right 245 | $newBits = $charmap[$encodedString[$c]] >> $unusedBitCount; 246 | $bitsWritten = 8; //$bitsWritten += $bitsNeeded; 247 | 248 | } else { 249 | // Final bits don't need to be shifted 250 | $newBits = $charmap[$encodedString[$c]]; 251 | $bitsWritten = 8; 252 | } 253 | 254 | $byte |= $newBits; 255 | 256 | if ($bitsWritten == 8 || $c == $lastNotatedIndex) { 257 | // Byte is ready to be written 258 | $rawString .= pack('C', $byte); 259 | 260 | if ($c != $lastNotatedIndex) { 261 | // Start the next byte 262 | $bitsWritten = $unusedBitCount; 263 | $byte = ($charmap[$encodedString[$c]] 264 | ^ ($newBits << $unusedBitCount)) << 8 - $bitsWritten; 265 | } 266 | } 267 | 268 | } elseif ($strict) { 269 | // Unable to decode character; abort 270 | return NULL; 271 | } 272 | } 273 | 274 | return $rawString; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /lib/GoogleAuthenticator.php: -------------------------------------------------------------------------------- 1 | getCode($secret,$time + $i) == $code) { 32 | return true; 33 | } 34 | } 35 | 36 | return false; 37 | 38 | } 39 | 40 | public function getCode($secret,$time = null) { 41 | 42 | if (!$time) { 43 | $time = floor(time() / 30); 44 | } 45 | $base32 = new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', TRUE, TRUE); 46 | $secret = $base32->decode($secret); 47 | 48 | $time = pack("N", $time); 49 | $time = str_pad($time,8, chr(0), STR_PAD_LEFT); 50 | 51 | $hash = hash_hmac('sha1',$time,$secret,true); 52 | $offset = ord(substr($hash,-1)); 53 | $offset = $offset & 0xF; 54 | 55 | $truncatedHash = self::hashToInt($hash, $offset) & 0x7FFFFFFF; 56 | $pinValue = str_pad($truncatedHash % self::$PIN_MODULO,6,"0",STR_PAD_LEFT);; 57 | return $pinValue; 58 | } 59 | 60 | protected function hashToInt($bytes, $start) { 61 | $input = substr($bytes, $start, strlen($bytes) - $start); 62 | $val2 = unpack("N",substr($input,0,4)); 63 | return $val2[1]; 64 | } 65 | 66 | public function getUrl($user, $hostname, $secret) { 67 | $url = sprintf("otpauth://totp/%s@%s?secret=%s", $user, $hostname, $secret); 68 | $encoder = "https://api.qrserver.com/v1/create-qr-code/?size=200x200&data="; 69 | $encoderURL = sprintf( "%sotpauth://totp/%s@%s&secret=%s",$encoder, $user, $hostname, $secret); 70 | 71 | return $encoderURL; 72 | 73 | } 74 | 75 | public function generateSecret() { 76 | $secret = ""; 77 | for($i = 1; $i<= self::$SECRET_LENGTH;$i++) { 78 | $c = rand(0,255); 79 | $secret .= pack("c",$c); 80 | } 81 | $base32 = new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', TRUE, TRUE); 82 | return $base32->encode($secret); 83 | 84 | 85 | } 86 | 87 | } 88 | 89 | -------------------------------------------------------------------------------- /tmpl/ask-for-otp.php: -------------------------------------------------------------------------------- 1 | 2 |
4 |
-------------------------------------------------------------------------------- /tmpl/loggedin.php: -------------------------------------------------------------------------------- 1 | 2 |3 | Hello getUsername(); ?> 4 |
5 | 8 | 9 |10 | Show QR Code 11 |
12 | 13 | 16 | 17 |18 | Logout 19 |
-------------------------------------------------------------------------------- /tmpl/login-error.php: -------------------------------------------------------------------------------- 1 |2 | Wrong username or password or token. 3 |
4 |5 | try again 6 |
-------------------------------------------------------------------------------- /tmpl/login.php: -------------------------------------------------------------------------------- 1 | 2 |4 |
-------------------------------------------------------------------------------- /tmpl/show-qr.php: -------------------------------------------------------------------------------- 1 |with the Google Authenticator App
4 | 5 |
6 | getUrl($user->getUsername(),$_SERVER['HTTP_HOST'],$secret);
9 | ?>
10 |
11 |
12 |