mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-03 14:37:02 +03:00
2-Step Verification (Google Authenticator)
Small fixes
This commit is contained in:
parent
cae0cc2f77
commit
419b47a81e
51 changed files with 1747 additions and 419 deletions
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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('')
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue