<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2015 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */


/**
 * flAuthTOTP Class
 *
 * Class to implement TOTP (Time-based One Time Password) authentication.
 * The codes generated are compatiable with Google Authenticator.
 *
 * @package fusionLib
 */
class flAuthTOTP {

	/**
	 * Generates a 16 digit secret key in base32 format
	 *
	 * @return string The secret.
	 */
	static function generateSecret() {
		$b32 = '234567QWERTYUIOPASDFGHJKLZXCVBNM';
		$s 	= '';

		for ($i = 0; $i < 16; $i++)
			$s .= $b32[rand(0,31)];

		return $s;
	}

	/**
	 * Get QR-Code URI by using google charts
	 *
	 * @param string $name The name to associate with the secret.
	 * @param string $secret The secret key.
	 * @return string The URI for the QR code.
	 */
	static function getQRCode($name, $secret, $size = 150) {
		return 'https://chart.googleapis.com/chart?chs=' . $size . 'x' . $size . '&amp;chld=M|0&amp;cht=qr&amp;chl=' . urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
	}

	/**
	 * Calculate the code, with given secret and point in time
	 *
	 * @param string $secret The secret key.
	 * @param int $timeSlice The time slice or null for the current time.
	 * @return string The 6 digit code.
	 */
	static function getCode($secret, $timeSlice = null) {
		$timeSlice = $timeSlice === null ? floor(time() / 30) : $timeSlice;

		$time = pack('N*', 0) . pack('N*', $timeSlice);
		$hash = hash_hmac ('sha1', $time, self::base32Decode($secret), true);
		$hashpart = substr($hash, ord(substr($hash, -1)) & 0x0F, 4);
		$value = unpack('N', $hashpart);
		$value = $value[1];
		$value = $value & 0x7FFFFFFF;

		return str_pad($value % pow(10, 6), 6, '0', STR_PAD_LEFT);
	}

	/**
	 * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now
	 *
	 * @param string $secret The secret.
	 * @param string $code The code to verify.
	 * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
	 * @return bool true if code is valid; else false.
	 */
	static function verifyCode($secret, $code, $discrepancy = 1) {
		$currentTimeSlice = floor(time() / 30);

		for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
			$calculatedCode = self::getCode($secret, $currentTimeSlice + $i);
			if ($calculatedCode == $code ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Decodes a base32 string into a binary string.
	 *
	 * @param string $b32 Base 32 string.
	 * @return string The decoded value.
	 * @throws Exception
	 */
	protected static function base32Decode($b32) {
		static $lut = array(
			'A' => 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5,
			'G' => 6, 'H' => 7, 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11,
			'M' => 12, 'N' => 13, 'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17,
			'S' => 18, 'T' => 19, 'U' => 20, 'V' => 21, 'W' => 22, 'X' => 23,
			'Y' => 24, 'Z' => 25, '2' => 26, '3' => 27, '4' => 28, '5' => 29,
			'6' => 30, '7' => 31
		);

		if(!preg_match('/^[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]+$/', $b32 = strtoupper($b32)))
			throw new Exception('Invalid characters in the base32 string.');

		$l = strlen($b32);
		$n = $j	= 0;
		$binary = '';

		for ($i = 0; $i < $l; $i++) {
			$n = $n << 5; 				// Move buffer left by 5 to make room
			$n = $n + $lut[$b32[$i]]; 	// Add value into buffer
			$j = $j + 5;				// Keep track of number of bits in buffer

			if ($j >= 8) {
				$j = $j - 8;
				$binary .= chr(($n & (0xFF << $j)) >> $j);
			}
		}

		return $binary;
	}
}
