Proper Oauth for Google Home

This commit is contained in:
JonatanRek 2020-05-17 01:27:06 +02:00
parent 1af11f3f58
commit fef3c1e57f
15 changed files with 403 additions and 421 deletions

View File

@ -13,6 +13,7 @@ $router->any('/logout', 'Logout');
$router->any('/automation', 'Automation');
$router->any('/setting', 'Setting');
$router->any('/ajax', 'Ajax');
$router->any('/oauth', 'Oauth');
$router->post('/api/login', 'AuthApi@login');
$router->post('/api/logout', 'AuthApi@logout');
@ -20,7 +21,7 @@ $router->post('/api/logout', 'AuthApi@logout');
$router->get('/api/devices', 'DevicesApi@default');
$router->get('/api/rooms', 'RoomsApi@default');
$router->get('/api/HA/auth', 'GoogleHomeApi@autorize');
$router->any('/api/HA/auth', 'Oauth');
$router->any('/api/HA', 'GoogleHomeApi@response');
// examples

View File

@ -1,7 +1,7 @@
<?php
class GoogleHomeApi {
class GoogleHomeApi{
static function response(){
//$this->requireAuth();
$json = file_get_contents('php://input');
$obj = json_decode($json, true);

26
app/api/RecordApi.php Normal file
View File

@ -0,0 +1,26 @@
<?php
class RecordApi extends ApiController{
public function default(){
//$this->requireAuth();
$response = [];
$roomIds = [];
$roomsData = RoomManager::getRoomsDefault();
foreach ($roomsData as $roomKey => $room) {
$roomIds[] = $room['room_id'];
}
$subDevicesData = SubDeviceManager::getSubdevicesByRoomIds($roomIds);
foreach ($roomsData as $roomKey => $roomData) {
$response[] = [
'room_id' => $roomData['room_id'],
'name' => $roomData['name'],
'widgets' => isset($subDevicesData[$roomData['room_id']]) ? $subDevicesData[$roomData['room_id']] : [],
];
}
$this->response($response);
}
}

View File

@ -0,0 +1,75 @@
<?php
global $userManager;
if (
isset($_POST['username']) &&
$_POST['username'] != '' &&
isset($_POST['password']) &&
$_POST['password'] != ''
){
$ota = false;
$userName = $_POST['username'];
$userPassword = $_POST['password'];
$state = $_POST["state"];
$clientId = $_POST["clientId"];
$ota = $userManager->haveOtaEnabled($userName);
if ($ota == "") {
$token = (new AuthManager)->getToken($userName,$userPassword, $clientId);
if (!$token) {
throw new Exception("Auth failed", 401);
}
$get = [
"access_token"=>$token,
"token_type"=>"Bearer",
"state"=>$state,
];
header('Location: ' . $_POST["redirectUrl"] . '#' . http_build_query($get));
die();
}
$_SESSION['USERNAME'] = $userName;
$_SESSION['PASSWORD'] = $userPassword;
$_SESSION['OTA'] = $ota;
$_SESSION['STATE'] = $state;
$_SESSION['REDIRECT'] = $_POST["redirectUrl"];
$_SESSION['CLIENT'] = $clientId;
} else if (
isset($_POST['otaCode']) &&
$_POST['otaCode'] != ''
) {
$otaCode = $_POST['otaCode'];
$otaSecret = $_POST['otaSecret'];
$userName = $_SESSION['USERNAME'];
$userPassword = $_SESSION['PASSWORD'];
$ota = $_SESSION['OTA'];
$oauthState = $_SESSION['STATE'];
$oauthRedirect = $_SESSION['REDIRECT'];
$oauthClientId = $_SESSION['CLIENT'];
$ga = new PHPGangsta_GoogleAuthenticator();
$checkResult = $ga->verifyCode($otaSecret, $otaCode, 2); // 2 = 2*30sec clock tolerance
if ($checkResult) {
$token = (new AuthManager)->getToken($userName,$userPassword, $oauthClientId);
if (!$token) {
throw new Exception("Auth failed", 401);
}
$get = [
"access_token"=>$token,
"token_type"=>"Bearer",
"state"=>$oauthState,
];
header('Location: ' . $oauthRedirect . '#' . http_build_query($get));
echo 'OK';
} else {
echo 'FAILED';
}
die();
}

View File

@ -14,9 +14,8 @@ if (isset($_POST) && !empty($_POST)){
header('Location: ' . BASEURL . 'setting');
die();
} else if (isset($_POST['submitEnableOta']) && $_POST['submitEnableOta'] != "") {
echo $otaCode = $_POST['otaCode'];
echo $otaSecret = $_POST['otaSecret'];
$otaCode = $_POST['otaCode'];
$otaSecret = $_POST['otaSecret'];
$ga = new PHPGangsta_GoogleAuthenticator();
$checkResult = $ga->verifyCode($otaSecret, $otaCode, 2); // 2 = 2*30sec clock tolerance

View File

@ -12,6 +12,11 @@ class GoogleHome {
//Google Compatibile Action Type
$actionType = GoogleHomeDeviceTypes::getAction($subDeviceData['type']);
if (strpos($deviceData['name'], 'Světlo') !== false || strpos($deviceData['name'], 'světlo') !== false) {
$actionType = 'action.devices.types.LIGHT';
}
$tempDevice = [
'id' => (string) $subDeviceData['subdevice_id'],
'type' => $actionType,
@ -78,181 +83,181 @@ class GoogleHome {
$deviceId['id'] => [
'online' => $online,
'status'=> $status,
]
];
]
];
if ($subDeviceData['type'] == "temp_cont"){
$tempDevice[$deviceId['id']]['thermostatMode'] = 'off';
if (RecordManager::getLastRecord($deviceId['id'])['value'] != 0) {
$tempDevice[$deviceId['id']]['thermostatMode'] = 'heat';
}
if ($subDeviceData['type'] == "temp_cont"){
$tempDevice[$deviceId['id']]['thermostatMode'] = 'off';
if (RecordManager::getLastRecord($deviceId['id'])['value'] != 0) {
$tempDevice[$deviceId['id']]['thermostatMode'] = 'heat';
}
$tempDevice[$deviceId['id']]['thermostatTemperatureAmbient'] = RecordManager::getLastRecord($deviceId['id'])['value'];
$tempDevice[$deviceId['id']]['thermostatTemperatureSetpoint'] = RecordManager::getLastRecord($deviceId['id'])['value'];
} else {
$tempDevice[$deviceId['id']]['on'] = $state;
}
$devices = $tempDevice;
if (count($devices)> 1){
$devices[] = $tempDevice;
}
}
$response = [
'requestId' => $requestId,
'payload' => [
'devices' => $devices,
],
];
$apiLogManager = new LogManager('../logs/api/HA/'. date("Y-m-d").'.log');
$apiLogManager->write("[API][$requestId] request response\n" . json_encode($response, JSON_PRETTY_PRINT), LogRecordType::INFO);
echo json_encode($response);
}
static function execute($requestId, $payload){
$commands = [];
foreach ($payload['commands'] as $key => $command) {
foreach ($command['devices'] as $key => $device) {
$executionCommand = $command['execution'][0];
if (isset($command['execution'][$key])) {
$executionCommand = $command['execution'][$key];
} else {
$tempDevice[$deviceId['id']]['on'] = $state;
}
$subDeviceId = $device['id'];
switch ($executionCommand['command']) {
case 'action.devices.commands.OnOff':
$commands[] = self::executeSwitch($subDeviceId, $executionCommand);
break;
case 'action.devices.commands.ThermostatTemperatureSetpoint':
$commands[] = self::executeTermostatValue($subDeviceId, $executionCommand);
break;
case 'action.devices.commands.ThermostatSetMode':
$commands[] = self::executeTermostatMode($subDeviceId, $executionCommand);
break;
$devices = $tempDevice;
if (count($devices)> 1){
$devices[] = $tempDevice;
}
}
$response = [
'requestId' => $requestId,
'payload' => [
'devices' => $devices,
],
];
$apiLogManager = new LogManager('../logs/api/HA/'. date("Y-m-d").'.log');
$apiLogManager->write("[API][$requestId] request response\n" . json_encode($response, JSON_PRETTY_PRINT), LogRecordType::INFO);
echo json_encode($response);
}
$response = [
'requestId' => $requestId,
'payload' => [
'commands' => $commands,
],
];
$apiLogManager = new LogManager('../logs/api/HA/'. date("Y-m-d").'.log');
$apiLogManager->write("[API][EXECUTE][$requestId]\n" . json_encode($response, JSON_PRETTY_PRINT), LogRecordType::INFO);
static function execute($requestId, $payload){
$commands = [];
echo json_encode($response);
}
foreach ($payload['commands'] as $key => $command) {
foreach ($command['devices'] as $key => $device) {
$executionCommand = $command['execution'][0];
if (isset($command['execution'][$key])) {
$executionCommand = $command['execution'][$key];
}
static function executeSwitch($subDeviceId, $executionCommand){
$value = 0;
$status = 'SUCCESS';
if ($executionCommand['params']['on']) $value = 1;
$subDeviceId = $device['id'];
RecordManager::createWithSubId($subDeviceId, $value);
switch ($executionCommand['command']) {
case 'action.devices.commands.OnOff':
$commands[] = self::executeSwitch($subDeviceId, $executionCommand);
break;
$executed = 0;
$waiting = 0;
foreach (RecordManager::getLastRecord($subDeviceId, 4) as $key => $value) {
if ($value['execuded'] == 1){
$executed++;
} else {
$waiting++;
case 'action.devices.commands.ThermostatTemperatureSetpoint':
$commands[] = self::executeTermostatValue($subDeviceId, $executionCommand);
break;
case 'action.devices.commands.ThermostatSetMode':
$commands[] = self::executeTermostatMode($subDeviceId, $executionCommand);
break;
}
}
}
}
if ($waiting < $executed){
$status = "PENDING";
} else {
$status = "OFFLINE";
$response = [
'requestId' => $requestId,
'payload' => [
'commands' => $commands,
],
];
$apiLogManager = new LogManager('../logs/api/HA/'. date("Y-m-d").'.log');
$apiLogManager->write("[API][EXECUTE][$requestId]\n" . json_encode($response, JSON_PRETTY_PRINT), LogRecordType::INFO);
echo json_encode($response);
}
$commandTemp = [
'ids' => [$subDeviceId],
'status' => $status,
'states' => [
'on' => $executionCommand['params']['on'],
],
];
return $commandTemp;
}
static function executeSwitch($subDeviceId, $executionCommand){
$value = 0;
$status = 'SUCCESS';
if ($executionCommand['params']['on']) $value = 1;
static function executeTermostatValue($subDeviceId, $executionCommand){
$value = 0;
$status = 'SUCCESS';
RecordManager::createWithSubId($subDeviceId, $value);
if (isset($executionCommand['params']['thermostatTemperatureSetpoint'])) {
$value = $executionCommand['params']['thermostatTemperatureSetpoint'];
}
RecordManager::createWithSubId($subDeviceId, $value);
$executed = 0;
$waiting = 0;
foreach (RecordManager::getLastRecord($subDeviceId, 4) as $key => $lastValue) {
if ($lastValue['execuded'] == 1){
$executed++;
} else {
$waiting++;
$executed = 0;
$waiting = 0;
foreach (RecordManager::getLastRecord($subDeviceId, 4) as $key => $value) {
if ($value['execuded'] == 1){
$executed++;
} else {
$waiting++;
}
}
if ($waiting < $executed){
$status = "PENDING";
} else {
$status = "OFFLINE";
}
$commandTemp = [
'ids' => [$subDeviceId],
'status' => $status,
'states' => [
'on' => $executionCommand['params']['on'],
],
];
return $commandTemp;
}
if ($waiting < $executed){
$status = "PENDING";
static function executeTermostatValue($subDeviceId, $executionCommand){
$value = 0;
$status = 'SUCCESS';
if (isset($executionCommand['params']['thermostatTemperatureSetpoint'])) {
$value = $executionCommand['params']['thermostatTemperatureSetpoint'];
}
RecordManager::createWithSubId($subDeviceId, $value);
$executed = 0;
$waiting = 0;
foreach (RecordManager::getLastRecord($subDeviceId, 4) as $key => $lastValue) {
if ($lastValue['execuded'] == 1){
$executed++;
} else {
$waiting++;
}
}
if ($waiting < $executed){
$status = "PENDING";
$status = "SUCCESS";
} else {
$status = "OFFLINE";
}
$commandTemp = [
'ids' => [$subDeviceId],
'status' => $status,
'states' => [
'thermostatMode' => 'heat',
'thermostatTemperatureSetpoint' => $value,
'thermostatTemperatureAmbient' => $value,
//ambient z dalšího zenzoru v roomu
],
];
return $commandTemp;
}
static function executeTermostatMode($subDeviceId, $executionCommand){
$mode = "off";
$value = 0;
$status = "SUCCESS";
} else {
$status = "OFFLINE";
}
$commandTemp = [
'ids' => [$subDeviceId],
'status' => $status,
'states' => [
'thermostatMode' => 'heat',
'thermostatTemperatureSetpoint' => $value,
'thermostatTemperatureAmbient' => $value,
//ambient z dalšího zenzoru v roomu
],
];
return $commandTemp;
}
static function executeTermostatMode($subDeviceId, $executionCommand){
$mode = "off";
$value = 0;
$status = "SUCCESS";
if (isset($executionCommand['params']['thermostatMode']) && $executionCommand['params']['thermostatMode'] != 'off') {
$mode = $executionCommand['params']['thermostatMode'];
$value = RecordManager::getLastRecordNotNull($subDeviceId)['value'];
}
RecordManager::createWithSubId($subDeviceId, $value);
$executed = 0;
$waiting = 0;
foreach (RecordManager::getLastRecord($subDeviceId, 4) as $key => $value) {
if ($value['execuded'] == 1){
$executed++;
} else {
$waiting++;
if (isset($executionCommand['params']['thermostatMode']) && $executionCommand['params']['thermostatMode'] != 'off') {
$mode = $executionCommand['params']['thermostatMode'];
$value = RecordManager::getLastRecordNotNull($subDeviceId)['value'];
}
}
if ($waiting < $executed){
$status = "PENDING";
}
$commandTemp = [
'ids' => [$subDeviceId],
'status' => $status,
'states' => [
'thermostatMode' => $mode
],
];
RecordManager::createWithSubId($subDeviceId, $value);
return $commandTemp;
$executed = 0;
$waiting = 0;
foreach (RecordManager::getLastRecord($subDeviceId, 4) as $key => $value) {
if ($value['execuded'] == 1){
$executed++;
} else {
$waiting++;
}
}
if ($waiting < $executed){
$status = "PENDING";
}
$commandTemp = [
'ids' => [$subDeviceId],
'status' => $status,
'states' => [
'thermostatMode' => $mode
],
];
return $commandTemp;
}
}
}

View File

@ -1,7 +1,11 @@
<?php
class AuthManager {
public function getToken($username, $password){
public function getToken($username, $password, $userAgent = null){
if ($userAgent == null) {
$userAgent = $this->headers['HTTP_USER_AGENT'];
}
$userManager = new UserManager();
if ($username != '' || $password != ''){
$userLogedIn = $userManager->loginNew($username, $password);
@ -10,7 +14,11 @@ class AuthManager {
// Create token header as a JSON string
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
// Create token payload as a JSON string
$payload = json_encode(['user_id' => $userLogedIn]);
$payload = json_encode([
'user_id' => $userLogedIn,
'exp' => date('Y-m-d H:i:s',strtotime("+90 Days")),
'iat' => date('Y-m-d H:i:s',time()),
]);
// Encode Header to Base64Url String
$base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header));
// Encode Payload to Base64Url String
@ -22,7 +30,17 @@ class AuthManager {
// Create JWT
$jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
return $jwt;
$token = [
'user_id' => $userLogedIn,
'user_agent' => $userAgent,
'token' => $jwt,
'expire' => date('Y-m-d H:i:s',strtotime("+90 Days")),
'issued' => date('Y-m-d H:i:s',time()),
];
if (Db::add ('tokens', $token)){
return $jwt;
}
}
}
return false;

View File

@ -206,5 +206,15 @@ class UserManager
return false;
}
}
public static function setOta($otaCode, $otaSecret){
$ga = new PHPGangsta_GoogleAuthenticator();
$checkResult = $ga->verifyCode($otaSecret, $otaCode, 2); // 2 = 2*30sec clock tolerance
if ($checkResult) {
self::setUserData('ota', $otaSecret);
return true;
}
return false;
}
}
?>

30
app/views/Oauth.php Normal file
View File

@ -0,0 +1,30 @@
<?php
class Oauth extends Template
{
function __construct()
{
global $userManager;
global $lang;
$template = new Template('oauth');
$template->prepare('baseDir', BASEDIR);
$template->prepare('title', 'Home');
$template->prepare('lang', $lang);
if (isset($_GET['redirect_uri'])) {
$template->prepare('responseType', $_GET['response_type']);
$template->prepare('redirectUrl', $_GET['redirect_uri']);
$template->prepare('clientId', $_GET['client_id']);
$template->prepare('state', $_GET['state']);
} else {
$template->prepare('responseType', $_POST['responseType']);
$template->prepare('redirectUrl', $_POST['redirectUrl']);
$template->prepare('clientId', $_POST['clientId']);
$template->prepare('state', $_POST['state']);
}
$template->render();
}
}

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<?php
$partial = new Partial('head');
$partial->prepare('baseDir',$BASEDIR);
$partial->render();
?>
<title><?php echo $TITLE ?></title>
</head>
<body class="no-transitions">
<?php
if (isset($ota) && $ota != '') {
$partial = new Partial('oauthLoginOta');
$partial->prepare('ota',$ota);
$partial->render();
} else {
$partial = new Partial('oauthLoginForm');
$partial->prepare('responseType',$RESPONSETYPE);
$partial->prepare('redirectUrl',$REDIRECTURL);
$partial->prepare('clientId',$CLIENTID);
$partial->prepare('state',$STATE);
$partial->render();
}
?>
<?php
$partial = new Partial('footer');
$partial->render();
?>
</body>
</html>

View File

@ -18,9 +18,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="./css/main.css?v2">
<link rel="stylesheet" href="./css/font-awesome.min.css">
<link rel="stylesheet" href="./css/modal.css">
<link rel="stylesheet" href="./css/pre.css">
<link rel="stylesheet" href="./css/loading.css">
<link rel="stylesheet" href="./css/override.css">
<link rel="stylesheet" href="<?php echo $BASEDIR; ?>public/css/main.css?v2">
<link rel="stylesheet" href="<?php echo $BASEDIR; ?>public/css/font-awesome.min.css">
<link rel="stylesheet" href="<?php echo $BASEDIR; ?>public/css/modal.css">
<link rel="stylesheet" href="<?php echo $BASEDIR; ?>public/css/pre.css">
<link rel="stylesheet" href="<?php echo $BASEDIR; ?>public/css/loading.css">
<link rel="stylesheet" href="<?php echo $BASEDIR; ?>public/css/override.css">

View File

@ -0,0 +1,22 @@
<div class="modal-container">
<div class="modal">
<h4 class="mb-4">Login</h4>
<form method="post">
<div class="field">
<div class="label">Name:</div>
<input class="input" type="text" name="username" placeholder="Jméno.."/>
</div>
<div class="field">
<div class="label">Password:</div>
<input class="input" type="password" name="password" placeholder="Heslo.."/>
</div>
<input type="hidden" name="responseType" value="<?php echo $RESPONSETYPE; ?>"/>
<input type="hidden" name="redirectUrl" value="<?php echo $REDIRECTURL; ?>"/>
<input type="hidden" name="clientId" value="<?php echo $CLIENTID; ?>"/>
<input type="hidden" name="state" value="<?php echo $STATE; ?>"/>
<input type="submit" class="button" name="login" value="Login"/>
</form>
</div>
</div>

View File

@ -0,0 +1,16 @@
<div class="modal-container">
<div class="modal">
<h4 class="mb-4">OTA</h4>
<form method="post">
<input type="hidden" name="otaSecret" value="<?php echo $OTA; ?>"/>
<div class="field">
<div class="label">Code:</div>
<?php
?>
<input class="input" type="text" name="otaCode" placeholder=""/>
</div>
<input type="submit" class="button" name="login" value="Login"/>
</form>
</div>
</div>

View File

@ -95,7 +95,6 @@
<h4 class="mb-4"><?php $LANGMNG->echo('t_ota') ?></h4>
<?php if (!empty($QRURL)) {?>
<img src="<?php echo $QRURL;?>" />
<?php echo $OTACODE; ?>
<form method="post" action="setting">
<div class="field">
<div class="label"><?php $LANGMNG->echo('l_gooleAutenticatorOtaCode') ?>:</div>

View File

@ -1,252 +0,0 @@
<?php
/**
* PHP Class for handling Google Authenticator 2-factor authentication.
*
* @author Michael Kliewe
* @copyright 2012 Michael Kliewe
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
*
* @link http://www.phpgangsta.de/
*/
class PHPGangsta_GoogleAuthenticator
{
protected $_codeLength = 6;
/**
* Create new secret.
* 16 characters, randomly chosen from the allowed base32 characters.
*
* @param int $secretLength
*
* @return string
*/
public function createSecret($secretLength = 16)
{
$validChars = $this->_getBase32LookupTable();
// Valid secret lengths are 80 to 640 bits
if ($secretLength < 16 || $secretLength > 128) {
throw new Exception('Bad secret length');
}
$secret = '';
$rnd = false;
if (function_exists('random_bytes')) {
$rnd = random_bytes($secretLength);
} elseif (function_exists('mcrypt_create_iv')) {
$rnd = mcrypt_create_iv($secretLength, MCRYPT_DEV_URANDOM);
} elseif (function_exists('openssl_random_pseudo_bytes')) {
$rnd = openssl_random_pseudo_bytes($secretLength, $cryptoStrong);
if (!$cryptoStrong) {
$rnd = false;
}
}
if ($rnd !== false) {
for ($i = 0; $i < $secretLength; ++$i) {
$secret .= $validChars[ord($rnd[$i]) & 31];
}
} else {
throw new Exception('No source of secure random');
}
return $secret;
}
/**
* Calculate the code, with given secret and point in time.
*
* @param string $secret
* @param int|null $timeSlice
*
* @return string
*/
public function getCode($secret, $timeSlice = null)
{
if ($timeSlice === null) {
$timeSlice = floor(time() / 30);
}
$secretkey = $this->_base32Decode($secret);
// Pack time into binary string
$time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
// Hash it with users secret key
$hm = hash_hmac('SHA1', $time, $secretkey, true);
// Use last nipple of result as index/offset
$offset = ord(substr($hm, -1)) & 0x0F;
// grab 4 bytes of the result
$hashpart = substr($hm, $offset, 4);
// Unpak binary value
$value = unpack('N', $hashpart);
$value = $value[1];
// Only 32 bits
$value = $value & 0x7FFFFFFF;
$modulo = pow(10, $this->_codeLength);
return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
}
/**
* Get QR-Code URL for image, from google charts.
*
* @param string $name
* @param string $secret
* @param string $title
* @param array $params
*
* @return string
*/
public function getQRCodeGoogleUrl($name, $secret, $title = null, $params = array())
{
$width = !empty($params['width']) && (int) $params['width'] > 0 ? (int) $params['width'] : 200;
$height = !empty($params['height']) && (int) $params['height'] > 0 ? (int) $params['height'] : 200;
$level = !empty($params['level']) && array_search($params['level'], array('L', 'M', 'Q', 'H')) !== false ? $params['level'] : 'M';
$urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
if (isset($title)) {
$urlencoded .= urlencode('&issuer='.urlencode($title));
}
return "https://api.qrserver.com/v1/create-qr-code/?data=$urlencoded&size=${width}x${height}&ecc=$level";
}
/**
* Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now.
*
* @param string $secret
* @param string $code
* @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
* @param int|null $currentTimeSlice time slice if we want use other that time()
*
* @return bool
*/
public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null)
{
if ($currentTimeSlice === null) {
$currentTimeSlice = floor(time() / 30);
}
if (strlen($code) != 6) {
return false;
}
for ($i = -$discrepancy; $i <= $discrepancy; ++$i) {
$calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
if ($this->timingSafeEquals($calculatedCode, $code)) {
return true;
}
}
return false;
}
/**
* Set the code length, should be >=6.
*
* @param int $length
*
* @return PHPGangsta_GoogleAuthenticator
*/
public function setCodeLength($length)
{
$this->_codeLength = $length;
return $this;
}
/**
* Helper class to decode base32.
*
* @param $secret
*
* @return bool|string
*/
protected function _base32Decode($secret)
{
if (empty($secret)) {
return '';
}
$base32chars = $this->_getBase32LookupTable();
$base32charsFlipped = array_flip($base32chars);
$paddingCharCount = substr_count($secret, $base32chars[32]);
$allowedValues = array(6, 4, 3, 1, 0);
if (!in_array($paddingCharCount, $allowedValues)) {
return false;
}
for ($i = 0; $i < 4; ++$i) {
if ($paddingCharCount == $allowedValues[$i] &&
substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) {
return false;
}
}
$secret = str_replace('=', '', $secret);
$secret = str_split($secret);
$binaryString = '';
for ($i = 0; $i < count($secret); $i = $i + 8) {
$x = '';
if (!in_array($secret[$i], $base32chars)) {
return false;
}
for ($j = 0; $j < 8; ++$j) {
$x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
}
$eightBits = str_split($x, 8);
for ($z = 0; $z < count($eightBits); ++$z) {
$binaryString .= (($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : '';
}
}
return $binaryString;
}
/**
* Get array with all 32 characters for decoding from/encoding to base32.
*
* @return array
*/
protected function _getBase32LookupTable()
{
return array(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
'=', // padding char
);
}
/**
* A timing safe equals comparison
* more info here: http://blog.ircmaxell.com/2014/11/its-all-about-time.html.
*
* @param string $safeString The internal (safe) value to be checked
* @param string $userString The user submitted (unsafe) value
*
* @return bool True if the two strings are identical
*/
private function timingSafeEquals($safeString, $userString)
{
if (function_exists('hash_equals')) {
return hash_equals($safeString, $userString);
}
$safeLen = strlen($safeString);
$userLen = strlen($userString);
if ($userLen != $safeLen) {
return false;
}
$result = 0;
for ($i = 0; $i < $userLen; ++$i) {
$result |= (ord($safeString[$i]) ^ ord($userString[$i]));
}
// They are only identical strings if $result is exactly 0...
return $result === 0;
}
}