From fef3c1e57fb15807a5c009f67a4268ac307bd5c1 Mon Sep 17 00:00:00 2001 From: JonatanRek Date: Sun, 17 May 2020 01:27:06 +0200 Subject: [PATCH] Proper Oauth for Google Home --- app/Routes.php | 3 +- app/api/GoogleHomeApi.php | 4 +- app/api/RecordApi.php | 26 ++ app/controllers/oauthController.php | 75 +++++ app/controllers/settingController.php | 5 +- app/models/GoogleHome.php | 311 +++++++++--------- app/models/managers/AuthManager.php | 24 +- app/models/managers/UserManager.php | 10 + app/views/Oauth.php | 30 ++ app/views/templates/oauth.phtml | 33 ++ app/views/templates/part/head.phtml | 12 +- app/views/templates/part/oauthLoginForm.phtml | 22 ++ app/views/templates/part/oauthLoginOta.phtml | 16 + app/views/templates/setting.phtml | 1 - library/vendor/GoogleAuthenticator.php | 252 -------------- 15 files changed, 403 insertions(+), 421 deletions(-) create mode 100644 app/api/RecordApi.php create mode 100644 app/controllers/oauthController.php create mode 100644 app/views/Oauth.php create mode 100644 app/views/templates/oauth.phtml create mode 100644 app/views/templates/part/oauthLoginForm.phtml create mode 100644 app/views/templates/part/oauthLoginOta.phtml delete mode 100644 library/vendor/GoogleAuthenticator.php diff --git a/app/Routes.php b/app/Routes.php index f973faa..6e7f198 100644 --- a/app/Routes.php +++ b/app/Routes.php @@ -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 diff --git a/app/api/GoogleHomeApi.php b/app/api/GoogleHomeApi.php index 40f67cd..a495669 100644 --- a/app/api/GoogleHomeApi.php +++ b/app/api/GoogleHomeApi.php @@ -1,7 +1,7 @@ requireAuth(); $json = file_get_contents('php://input'); $obj = json_decode($json, true); diff --git a/app/api/RecordApi.php b/app/api/RecordApi.php new file mode 100644 index 0000000..6c313ae --- /dev/null +++ b/app/api/RecordApi.php @@ -0,0 +1,26 @@ +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); + } +} diff --git a/app/controllers/oauthController.php b/app/controllers/oauthController.php new file mode 100644 index 0000000..9a208f7 --- /dev/null +++ b/app/controllers/oauthController.php @@ -0,0 +1,75 @@ +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(); +} diff --git a/app/controllers/settingController.php b/app/controllers/settingController.php index f31a8de..068aecb 100644 --- a/app/controllers/settingController.php +++ b/app/controllers/settingController.php @@ -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 diff --git a/app/models/GoogleHome.php b/app/models/GoogleHome.php index 9d66421..919799d 100644 --- a/app/models/GoogleHome.php +++ b/app/models/GoogleHome.php @@ -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; + } } -} diff --git a/app/models/managers/AuthManager.php b/app/models/managers/AuthManager.php index 88c96a6..65a71f1 100644 --- a/app/models/managers/AuthManager.php +++ b/app/models/managers/AuthManager.php @@ -1,7 +1,11 @@ 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; diff --git a/app/models/managers/UserManager.php b/app/models/managers/UserManager.php index 05fd3ef..1bdc2a6 100644 --- a/app/models/managers/UserManager.php +++ b/app/models/managers/UserManager.php @@ -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; + } } ?> diff --git a/app/views/Oauth.php b/app/views/Oauth.php new file mode 100644 index 0000000..0a7fb9a --- /dev/null +++ b/app/views/Oauth.php @@ -0,0 +1,30 @@ +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(); + } +} diff --git a/app/views/templates/oauth.phtml b/app/views/templates/oauth.phtml new file mode 100644 index 0000000..c754f96 --- /dev/null +++ b/app/views/templates/oauth.phtml @@ -0,0 +1,33 @@ + + + + prepare('baseDir',$BASEDIR); + $partial->render(); + ?> + <?php echo $TITLE ?> + + + + 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(); + } + ?> + + render(); + ?> + + diff --git a/app/views/templates/part/head.phtml b/app/views/templates/part/head.phtml index 1555607..df28bf5 100644 --- a/app/views/templates/part/head.phtml +++ b/app/views/templates/part/head.phtml @@ -18,9 +18,9 @@ - - - - - - + + + + + + diff --git a/app/views/templates/part/oauthLoginForm.phtml b/app/views/templates/part/oauthLoginForm.phtml new file mode 100644 index 0000000..f68a8a7 --- /dev/null +++ b/app/views/templates/part/oauthLoginForm.phtml @@ -0,0 +1,22 @@ + diff --git a/app/views/templates/part/oauthLoginOta.phtml b/app/views/templates/part/oauthLoginOta.phtml new file mode 100644 index 0000000..99b2aab --- /dev/null +++ b/app/views/templates/part/oauthLoginOta.phtml @@ -0,0 +1,16 @@ + diff --git a/app/views/templates/setting.phtml b/app/views/templates/setting.phtml index 18ac4f2..688047c 100644 --- a/app/views/templates/setting.phtml +++ b/app/views/templates/setting.phtml @@ -95,7 +95,6 @@

echo('t_ota') ?>

-
echo('l_gooleAutenticatorOtaCode') ?>:
diff --git a/library/vendor/GoogleAuthenticator.php b/library/vendor/GoogleAuthenticator.php deleted file mode 100644 index bf7d116..0000000 --- a/library/vendor/GoogleAuthenticator.php +++ /dev/null @@ -1,252 +0,0 @@ -_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; - } -}