2-Step Verification (Google Authenticator)

Small fixes
This commit is contained in:
RainLoop Team 2014-04-05 23:48:22 +04:00
parent cae0cc2f77
commit 419b47a81e
51 changed files with 1747 additions and 419 deletions

View file

@ -0,0 +1,201 @@
<?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();
unset($validChars[32]);
$secret = '';
for ($i = 0; $i < $secretLength; $i++) {
$secret .= $validChars[array_rand($validChars)];
}
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
* @return string
*/
public function getQRCodeGoogleUrl($name, $secret) {
$urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.'';
}
/**
* 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)
* @return bool
*/
public function verifyCode($secret, $code, $discrepancy = 1)
{
$currentTimeSlice = floor(time() / 30);
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
$calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
if ($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;
}
/**
* Helper class to encode base32
*
* @param string $secret
* @param bool $padding
* @return string
*/
protected function _base32Encode($secret, $padding = true)
{
if (empty($secret)) return '';
$base32chars = $this->_getBase32LookupTable();
$secret = str_split($secret);
$binaryString = "";
for ($i = 0; $i < count($secret); $i++) {
$binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT);
}
$fiveBitBinaryArray = str_split($binaryString, 5);
$base32 = "";
$i = 0;
while ($i < count($fiveBitBinaryArray)) {
$base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)];
$i++;
}
if ($padding && ($x = strlen($binaryString) % 40) != 0) {
if ($x == 8) $base32 .= str_repeat($base32chars[32], 6);
elseif ($x == 16) $base32 .= str_repeat($base32chars[32], 4);
elseif ($x == 24) $base32 .= str_repeat($base32chars[32], 3);
elseif ($x == 32) $base32 .= $base32chars[32];
}
return $base32;
}
/**
* 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
);
}
}

View file

@ -509,7 +509,7 @@ class Actions
if (null === $this->oTwoFactorAuthProvider)
{
$this->oTwoFactorAuthProvider = new \RainLoop\Providers\TwoFactorAuth(
$this->fabrica('two-factor-auth')
$this->Config()->Get('security', 'allow_two_factor_auth', false) ? $this->fabrica('two-factor-auth') : null
);
}
@ -932,6 +932,7 @@ class Actions
'LoginCss' => $oConfig->Get('branding', 'login_css', ''),
'Token' => $oConfig->Get('security', 'csrf_protection', false) ? \RainLoop\Utils::GetCsrfToken() : '',
'OpenPGP' => $oConfig->Get('security', 'openpgp', false),
'AllowTwoFactorAuth' => (bool) $oConfig->Get('security', 'allow_two_factor_auth', false),
'InIframe' => (bool) $oConfig->Get('labs', 'in_iframe', false),
'AllowAdminPanel' => (bool) $oConfig->Get('security', 'allow_admin_panel', true),
'AllowHtmlEditorSourceButton' => (bool) $oConfig->Get('labs', 'allow_html_editor_source_button', false),
@ -1141,6 +1142,7 @@ class Actions
$aResult['ReplyTo'] = '';
$aResult['Signature'] = '';
$aResult['SignatureToAll'] = false;
$aResult['EnableTwoFactor'] = false;
$aResult['ParentEmail'] = '';
$aResult['InterfaceAnimation'] = \RainLoop\Enumerations\InterfaceAnimation::NORMAL;
$aResult['CustomThemeType'] = \RainLoop\Enumerations\CustomThemeType::LIGHT;
@ -1181,6 +1183,7 @@ class Actions
$aResult['ReplyTo'] = $oSettings->GetConf('ReplyTo', $aResult['ReplyTo']);
$aResult['Signature'] = $oSettings->GetConf('Signature', $aResult['Signature']);
$aResult['SignatureToAll'] = !!$oSettings->GetConf('SignatureToAll', $aResult['SignatureToAll']);
$aResult['EnableTwoFactor'] = !!$oSettings->GetConf('EnableTwoFactor', $aResult['EnableTwoFactor']);
$aResult['ParentEmail'] = $oAccount->ParentEmail();
}
@ -1356,27 +1359,36 @@ class Actions
if ($oAccount && $this->TwoFactorAuthProvider()->IsActive())
{
$oSettings = $this->SettingsProvider()->Load($oAccount);
if ($oSettings)
$aData = $this->getTwoFactorInfo($oAccount->ParentEmailHelper());
if ($aData && isset($aData['IsSet'], $aData['Enable']) && !empty($aData['Secret']) && $aData['IsSet'] && $aData['Enable'])
{
$sTwoFactorAuthSecret = $oSettings->GetConf('TwoFactorAuthEnabled', false) ?
$oSettings->GetConf('TwoFactorAuthSecret', '') : '';
if (!empty($sTwoFactorAuthSecret))
$sTwoFactorAuthCode = \trim($sTwoFactorAuthCode);
if (empty($sTwoFactorAuthCode))
{
if (empty($sTwoFactorAuthCode))
$this->Logger()->Write('TwoFactorAuth: Required Code for '.$oAccount->ParentEmailHelper().' account.');
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountTwoFactorAuthRequired);
}
else
{
$this->Logger()->Write('TwoFactorAuth: Verify Code for '.$oAccount->ParentEmailHelper().' account.');
$bGood = false;
if (6 < \strlen($sTwoFactorAuthCode) && !empty($aData['BackupCodes']))
{
$this->Logger()->Write('TwoFactorAuth: Required Code for '.$oAccount->Email().' account.');
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountTwoFactorAuthRequired);
}
else
{
$this->Logger()->Write('TwoFactorAuth: Verify Code for '.$oAccount->Email().' account.');
if (!$this->TwoFactorAuthProvider()->VerifyCode($sTwoFactorAuthSecret, $sTwoFactorAuthCode))
$aBackupCodes = \explode(' ', \trim(\preg_replace('/[^\d]+/', ' ', $aData['BackupCodes'])));
$bGood = \in_array($sTwoFactorAuthCode, $aBackupCodes);
if ($bGood)
{
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountTwoFactorAuthError);
$this->removeBackupCodeFromTwoFactorInfo($oAccount->ParentEmailHelper(), $sTwoFactorAuthCode);
}
}
if (!$bGood && !$this->TwoFactorAuthProvider()->VerifyCode($aData['Secret'], $sTwoFactorAuthCode))
{
$this->loginErrorDelay();
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountTwoFactorAuthError);
}
}
}
}
@ -1420,7 +1432,7 @@ class Actions
$sLanguage = $this->GetActionParam('Language', '');
$bSignMe = '1' === $this->GetActionParam('SignMe', '0');
$sTwoFactorAuthCode = $this->GetActionParam('TwoFactorAuthCode', '');
$sAdditionalCode = $this->GetActionParam('AdditionalCode', '');
$this->Logger()->AddSecret($sPassword);
@ -1430,7 +1442,7 @@ class Actions
{
$oAccount = $this->LoginProcess($sEmail, $sLogin, $sPassword,
$bSignMe ? \md5(\microtime(true).APP_SALT.\rand(10000, 99999).$sEmail) : '',
$sTwoFactorAuthCode);
$sAdditionalCode);
}
catch (\RainLoop\Exceptions\ClientException $oException)
{
@ -2054,6 +2066,7 @@ class Actions
$this->setConfigFromParams($oConfig, 'TokenProtection', 'security', 'csrf_protection', 'bool');
$this->setConfigFromParams($oConfig, 'OpenPGP', 'security', 'openpgp', 'bool');
$this->setConfigFromParams($oConfig, 'AllowTwoFactorAuth', 'security', 'allow_two_factor_auth', 'bool');
$this->setConfigFromParams($oConfig, 'EnabledPlugins', 'plugins', 'enable', 'bool');
$this->setConfigFromParams($oConfig, 'GoogleEnable', 'social', 'google_enable', 'bool');
@ -3199,6 +3212,7 @@ class Actions
$this->setSettingsFromParams($oSettings, 'ReplyTo', 'string');
$this->setSettingsFromParams($oSettings, 'Signature', 'string');
$this->setSettingsFromParams($oSettings, 'SignatureToAll', 'bool');
$this->setSettingsFromParams($oSettings, 'EnableTwoFactor', 'bool');
$this->setSettingsFromParams($oSettings, 'CustomThemeImg', 'string');
@ -4545,6 +4559,236 @@ class Actions
return $this->DefaultResponse(__FUNCTION__, $aQuota);
}
private function getTwoFactorInfo($sEmail, $bRemoveSecret = false)
{
$mData = null;
$aResult = array(
'User' => '',
'IsSet' => false,
'Enable' => false,
'Secret' => '',
'Url' => '',
'BackupCodes' => ''
);
if (!empty($sEmail))
{
$aResult['User'] = $sEmail;
$sData = $this->StorageProvider()->Get(null,
\RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY,
'TwoFactorAuth/User/'.$sEmail.'/Data/'
);
if ($sData)
{
$mData = \RainLoop\Utils::DecodeKeyValues($sData);
}
}
if (\is_array($mData) && !empty($aResult['User']) &&
!empty($mData['User']) && !empty($mData['Secret']) &&
!empty($mData['BackupCodes']) && $sEmail === $mData['User'])
{
$aResult['IsSet'] = true;
$aResult['Enable'] = isset($mData['Enable']) ? !!$mData['Enable'] : false;
$aResult['Secret'] = $mData['Secret'];
$aResult['BackupCodes'] = $mData['BackupCodes'];
$aResult['Url'] = $this->TwoFactorAuthProvider()->GetQRCodeGoogleUrl(
$aResult['User'], $aResult['Secret'], $this->Config()->Get('webmail', 'title', ''));
}
if ($bRemoveSecret)
{
if (isset($aResult['Secret']))
{
unset($aResult['Secret']);
}
if (isset($aResult['Url']))
{
unset($aResult['Url']);
}
if (isset($aResult['BackupCodes']))
{
unset($aResult['BackupCodes']);
}
}
return $aResult;
}
/**
* @param string $sEmail
* @param string $sCode
*
* @return bool
*/
private function removeBackupCodeFromTwoFactorInfo($sEmail, $sCode)
{
if (empty($sEmail) || empty($sCode))
{
return false;
}
$sData = $this->StorageProvider()->Get(null,
\RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY,
'TwoFactorAuth/User/'.$sEmail.'/Data/'
);
if ($sData)
{
$mData = \RainLoop\Utils::DecodeKeyValues($sData);
if (!empty($mData['BackupCodes']))
{
$sBackupCodes = \preg_replace('/[^\d]+/', ' ', ' '.$mData['BackupCodes'].' ');
$sBackupCodes = \str_replace(' '.$sCode.' ', '', $sBackupCodes);
$mData['BackupCodes'] = \trim(\preg_replace('/[^\d]+/', ' ', $sBackupCodes));
return $this->StorageProvider()->Put(null,
\RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY,
'TwoFactorAuth/User/'.$sEmail.'/Data/',
\RainLoop\Utils::EncodeKeyValues($mData)
);
}
}
return false;
}
/**
* @return array
*/
public function DoGetTwoFactorInfo()
{
if (!$this->TwoFactorAuthProvider()->IsActive())
{
return $this->FalseResponse(__FUNCTION__);
}
$oAccount = $this->getAccountFromToken();
return $this->DefaultResponse(__FUNCTION__,
$this->getTwoFactorInfo($oAccount->ParentEmailHelper(), true));
}
/**
* @return array
*/
public function DoCreateTwoFactorSecret()
{
if (!$this->TwoFactorAuthProvider()->IsActive())
{
return $this->FalseResponse(__FUNCTION__);
}
$oAccount = $this->getAccountFromToken();
$sEmail = $oAccount->ParentEmailHelper();
$sSecret = $this->TwoFactorAuthProvider()->CreateSecret();
$aCodes = array();
for ($iIndex = 9; $iIndex > 0; $iIndex--)
{
$aCodes[] = \rand(100000000, 900000000);
}
$this->StorageProvider()->Put(null,
\RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY,
'TwoFactorAuth/User/'.$sEmail.'/Data/',
\RainLoop\Utils::EncodeKeyValues(array(
'User' => $sEmail,
'Enable' => false,
'Secret' => $sSecret,
'BackupCodes' => \implode(' ', $aCodes)
))
);
\sleep(1);
return $this->DefaultResponse(__FUNCTION__,
$this->getTwoFactorInfo($sEmail));
}
/**
* @return array
*/
public function DoEnableTwoFactor()
{
if (!$this->TwoFactorAuthProvider()->IsActive())
{
return $this->FalseResponse(__FUNCTION__);
}
$oAccount = $this->getAccountFromToken();
$sEmail = $oAccount->ParentEmailHelper();
$bResult = false;
$mData = $this->getTwoFactorInfo($sEmail);
if (isset($mData['Secret'], $mData['BackupCodes']))
{
$bResult = $this->StorageProvider()->Put(null,
\RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY,
'TwoFactorAuth/User/'.$sEmail.'/Data/',
\RainLoop\Utils::EncodeKeyValues(array(
'User' => $sEmail,
'Enable' => '1' === \trim($this->GetActionParam('Enable', '0')),
'Secret' => $mData['Secret'],
'BackupCodes' => $mData['BackupCodes']
))
);
}
return $this->DefaultResponse(__FUNCTION__, $bResult);
}
/**
* @return array
*/
public function DoTestTwoFactorInfo()
{
if (!$this->TwoFactorAuthProvider()->IsActive())
{
return $this->FalseResponse(__FUNCTION__);
}
$oAccount = $this->getAccountFromToken();
$sCode = \trim($this->GetActionParam('Code', ''));
$oData = $this->getTwoFactorInfo($oAccount->ParentEmailHelper());
$sSecret = !empty($oData['Secret']) ? $oData['Secret'] : '';
\sleep(1);
return $this->DefaultResponse(__FUNCTION__,
$this->TwoFactorAuthProvider()->VerifyCode($sSecret, $sCode));
}
/**
* @return array
*/
public function DoClearTwoFactorInfo()
{
if (!$this->TwoFactorAuthProvider()->IsActive())
{
return $this->FalseResponse(__FUNCTION__);
}
$oAccount = $this->getAccountFromToken();
$this->StorageProvider()->Clear(null,
\RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY,
'TwoFactorAuth/User/'.$oAccount->ParentEmailHelper().'/Data/'
);
return $this->DefaultResponse(__FUNCTION__,
$this->getTwoFactorInfo($oAccount->ParentEmailHelper(), true));
}
/**
* @return array
*/

View file

@ -105,6 +105,7 @@ class Application extends \RainLoop\Config\AbstractConfig
'admin_login' => array('admin', 'Login and password for web admin panel'),
'admin_password' => array('12345'),
'allow_admin_panel' => array(true, 'Access settings'),
'allow_two_factor_auth' => array(false),
'admin_panel_host' => array(''),
'core_install_access_domains' => array('')
),

View file

@ -79,6 +79,8 @@ class Notifications
self::SocialGoogleLoginAccessDisable => 'SocialGoogleLoginAccessDisable',
self::DomainNotAllowed => 'DomainNotAllowed',
self::AccountNotAllowed => 'AccountNotAllowed',
self::AccountTwoFactorAuthRequired => 'AccountTwoFactorAuthRequired',
self::AccountTwoFactorAuthError => 'AccountTwoFactorAuthError',
self::CantGetMessageList => 'CantGetMessageList',
self::CantGetMessage => 'CantGetMessage',
self::CantDeleteMessage => 'CantDeleteMessage',

View file

@ -27,9 +27,37 @@ class TwoFactorAuth extends \RainLoop\Providers\AbstractProvider
return $this->oDriver instanceof \RainLoop\Providers\TwoFactorAuth\TwoFactorAuthInterface;
}
/**
* @param string $sName
* @param string $sSecret
* @param string $sTitle = ''
*
* @return string
*/
public function GetQRCodeGoogleUrl($sName, $sSecret, $sTitle = '')
{
$sUrl = sprintf('otpauth://%s/%s?secret=%s&issuer=%s', 'totp', urlencode($sName), $sSecret, urlencode($sTitle));
return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.\urlencode($sUrl);
}
/**
* @return string
*/
public function CreateSecret()
{
$sResult = '';
if ($this->IsActive())
{
$sResult = $this->oDriver->CreateSecret();
}
return $sResult;
}
/**
* @param string $sSecret
* @param string $sCode
*
* @return bool
*/
public function VerifyCode($sSecret, $sCode)

View file

@ -1,206 +1,33 @@
<?php
namespace RainLoop\Providers\TwoFactorAuth;
class GoogleTwoFactorAuth
extends \RainLoop\Providers\TwoFactorAuth\AbstractTwoFactorAuth
implements \RainLoop\Providers\TwoFactorAuth\TwoFactorAuthInterface
{
private $iCodeLength = 6;
/**
* @return string
*/
public function Label()
{
return 'Google Authenticator Code';
}
/**
* Get QR-Code URL for image, from google charts
*
* Function from 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/
*
* @param string $sName
* @param string $sSecret
* @param string $sTitle
* @return string
*/
private function getQRCodeGoogleUrl($sName, $sSecret, $sTitle = null)
{
$sUrlEncoded = \urlencode('otpauth://totp/'.$sName.'?secret='.$sSecret.'');
if(null !== $sTitle)
{
$sUrlEncoded .= \urlencode('&issuer='.$sTitle);
}
return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$sUrlEncoded.'';
}
/**
* Get array with all 32 characters for decoding from/encoding to base32
*
* Function from 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/
*
* @return array
*/
private 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
);
}
/**
* Helper class to decode base32
*
* Function from 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/
*
* @param $sSecret
*
* @return bool|string
*/
private function base32Decode($sSecret)
{
if (empty($sSecret))
{
return '';
}
$aBase32chars = $this->getBase32LookupTable();
$aBase32charsFlipped = \array_flip($aBase32chars);
$iPaddingCharCount = \substr_count($sSecret, $aBase32chars[32]);
$aAllowedValues = array(6, 4, 3, 1, 0);
if (!\in_array($iPaddingCharCount, $aAllowedValues))
{
return false;
}
for ($iIndex = 0; $iIndex < 4; $iIndex++)
{
if ($iPaddingCharCount === $aAllowedValues[$iIndex] &&
\substr($sSecret, -($aAllowedValues[$iIndex])) !== \str_repeat($aBase32chars[32], $aAllowedValues[$iIndex]))
{
return false;
}
}
$sSecret = \str_replace('=', '', $sSecret);
$sSecret = \str_split($sSecret);
$sBinaryString = '';
for ($iIndex = 0; $iIndex < \count($sSecret); $iIndex = $iIndex + 8)
{
$sX = '';
if (!\in_array($sSecret[$iIndex], $aBase32chars))
{
return false;
}
for ($iJ = 0; $iJ < 8; $iJ++)
{
$sX .= \str_pad(\base_convert(@$aBase32charsFlipped[@$sSecret[$iIndex + $iJ]], 10, 2), 5, '0', STR_PAD_LEFT);
}
$eightBits = \str_split($sX, 8);
for ($iZ = 0; $iZ < \count($eightBits); $iZ++)
{
$sBinaryString .= (($y = \chr(\base_convert($eightBits[$iZ], 2, 10))) || ord($y) == 48 ) ? $y : '';
}
}
return $sBinaryString;
}
/**
* Calculate the code, with given secret and point in time
*
* Function from 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/
*
* @param string $sSecret
* @param int|null $mTimeSlice
*
* @return string
*/
private function getCode($sSecret, $mTimeSlice = null)
{
if (null === $mTimeSlice)
{
$mTimeSlice = \floor(\time() / 30);
}
$sSecretKey = $this->base32Decode($sSecret);
// Pack time into binary string
$sTime = \chr(0).\chr(0).\chr(0).\chr(0).\pack('N*', $mTimeSlice);
// Hash it with users secret key
$sHm = \hash_hmac('SHA1', $sTime, $sSecretKey, true);
// Use last nipple of result as index/offset
$iOffset = \ord(\substr($sHm, -1)) & 0x0F;
// grab 4 bytes of the result
$sHashPart = \substr($sHm, $iOffset, 4);
// Unpak binary value
$sValue = \unpack('N', $sHashPart);
$sValue = $sValue{1};
// Only 32 bits
$sValue = $sValue & 0x7FFFFFFF;
$iMod = \pow(10, $this->iCodeLength);
return \str_pad($sValue % $iMod, $this->iCodeLength, '0', STR_PAD_LEFT);
}
/**
* Check if the code is correct. This will accept codes starting
* from $iDiscrepancy * 30sec ago to $iDiscrepancy * 30sec from now
*
* @param string $sSecret
* @param string $sCode
*
* @return bool
*/
public function VerifyCode($sSecret, $sCode)
{
$iDiscrepancy = 1;
$iTimeSlice = \floor(\time() / 30);
for ($iIndex = -$iDiscrepancy; $iIndex <= $iDiscrepancy; $iIndex++)
{
if ($this->getCode($sSecret, $iTimeSlice + $iIndex) === $sCode)
{
return true;
}
}
return false;
}
}
<?php
namespace RainLoop\Providers\TwoFactorAuth;
class GoogleTwoFactorAuth
extends \RainLoop\Providers\TwoFactorAuth\AbstractTwoFactorAuth
implements \RainLoop\Providers\TwoFactorAuth\TwoFactorAuthInterface
{
/**
* @param string $sSecret
* @param string $sCode
*
* @return bool
*/
public function VerifyCode($sSecret, $sCode)
{
include_once APP_VERSION_ROOT_PATH.'app/libraries/PHPGangsta/GoogleAuthenticator.php';
$oGoogleAuthenticator = new \PHPGangsta_GoogleAuthenticator();
return $oGoogleAuthenticator->verifyCode($sSecret, $sCode);
}
/**
* @return string
*/
public function CreateSecret()
{
include_once APP_VERSION_ROOT_PATH.'app/libraries/PHPGangsta/GoogleAuthenticator.php';
$oGoogleAuthenticator = new \PHPGangsta_GoogleAuthenticator();
return $oGoogleAuthenticator->createSecret();
}
}

View file

@ -1,13 +1,14 @@
<?php
namespace RainLoop\Providers\TwoFactorAuth;
interface TwoFactorAuthInterface
{
/**
* @param string $sSecret
* @param string $sCode
* @return bool
*/
public function VerifyCode($sSecret, $sCode);
}
<?php
namespace RainLoop\Providers\TwoFactorAuth;
interface TwoFactorAuthInterface
{
/**
* @param string $sSecret
* @param string $sCode
*
* @return bool
*/
public function VerifyCode($sSecret, $sCode);
}