Two Factor Authentication (first look)

knockoutjs 3.1.0
New icons (archive, buy, filter)
Many small fixes.
This commit is contained in:
RainLoop Team 2014-03-28 20:02:39 +04:00
parent 32aedce2a0
commit a158164e80
24 changed files with 917 additions and 420 deletions

View file

@ -86,6 +86,11 @@ class Actions
*/
private $oChangePasswordProvider;
/**
* @var \RainLoop\Providers\TwoFactorAuth
*/
private $oTwoFactorAuthProvider;
/**
* @var \RainLoop\Config\Application
*/
@ -118,6 +123,7 @@ class Actions
$this->oPersonalAddressBookProvider = null;
$this->oSuggestionsProvider = null;
$this->oChangePasswordProvider = null;
$this->oTwoFactorAuthProvider = null;
$this->sSpecAuthToken = '';
@ -247,6 +253,10 @@ class Actions
case 'change-password':
// \RainLoop\Providers\ChangePassword\ChangePasswordInterface
break;
case 'two-factor-auth':
// \RainLoop\Providers\TwoFactorAuth\TwoFactorAuthInterface
$oResult = new \RainLoop\Providers\TwoFactorAuth\GoogleTwoFactorAuth();
break;
}
}
@ -491,6 +501,21 @@ class Actions
return $this->oChangePasswordProvider;
}
/**
* @return \RainLoop\Providers\TwoFactorAuth
*/
public function TwoFactorAuthProvider()
{
if (null === $this->oTwoFactorAuthProvider)
{
$this->oTwoFactorAuthProvider = new \RainLoop\Providers\TwoFactorAuth(
$this->fabrica('two-factor-auth')
);
}
return $this->oTwoFactorAuthProvider;
}
/**
* @return \RainLoop\Providers\Storage
*/
@ -1284,11 +1309,12 @@ class Actions
* @param string $sLogin
* @param string $sPassword
* @param string $sSignMeToken = ''
* @param string $sTwoFactorAuthCode = ''
*
* @return \RainLoop\Account
* @throws \RainLoop\Exceptions\ClientException
*/
public function LoginProcess(&$sEmail, &$sLogin, &$sPassword, $sSignMeToken = '')
public function LoginProcess(&$sEmail, &$sLogin, &$sPassword, $sSignMeToken = '', $sTwoFactorAuthCode = '')
{
if (false === \strpos($sEmail, '@') && 0 < \strlen(\trim($this->Config()->Get('login', 'default_domain', ''))))
{
@ -1327,6 +1353,33 @@ class Actions
}
}
if ($oAccount && $this->TwoFactorAuthProvider()->IsActive())
{
$oSettings = $this->SettingsProvider()->Load($oAccount);
if ($oSettings)
{
$sTwoFactorAuthSecret = $oSettings->GetConf('TwoFactorAuthEnabled', false) ?
$oSettings->GetConf('TwoFactorAuthSecret', '') : '';
if (!empty($sTwoFactorAuthSecret))
{
if (empty($sTwoFactorAuthCode))
{
$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))
{
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountTwoFactorAuthError);
}
}
}
}
}
try
{
$this->MailClient()
@ -1365,11 +1418,33 @@ class Actions
$sPassword = $this->GetActionParam('Password', '');
$sLanguage = $this->GetActionParam('Language', '');
$bSignMe = '1' === $this->GetActionParam('SignMe', '0');
$sTwoFactorAuthCode = $this->GetActionParam('TwoFactorAuthCode', '');
$this->Logger()->AddSecret($sPassword);
$oAccount = $this->LoginProcess($sEmail, $sLogin, $sPassword,
$bSignMe ? \md5(\microtime(true).APP_SALT.\rand(10000, 99999).$sEmail) : '');
$oAccount = null;
try
{
$oAccount = $this->LoginProcess($sEmail, $sLogin, $sPassword,
$bSignMe ? \md5(\microtime(true).APP_SALT.\rand(10000, 99999).$sEmail) : '',
$sTwoFactorAuthCode);
}
catch (\RainLoop\Exceptions\ClientException $oException)
{
if ($oException &&
\RainLoop\Notifications::AccountTwoFactorAuthRequired === $oException->getCode())
{
return $this->DefaultResponse(__FUNCTION__, true, array(
'TwoFactorAuth' => true
));
}
else
{
throw $oException;
}
}
$this->AuthProcess($oAccount);
@ -5234,7 +5309,6 @@ class Actions
* @param resource $rFile
* @param string $sFileStart
*
*
* @return int
*/
private function importContactsFromVcfFile($oAccount, $rFile, $sFileStart)
@ -6122,7 +6196,7 @@ class Actions
/**
* @param string $sKey
* @param mixed $mDefaul = null
* @param mixed $mDefault = null
*
* @return mixed
*/

View file

@ -1,9 +0,0 @@
<?php
namespace RainLoop\Exceptions;
/**
* @category RainLoop
* @package Exceptions
*/
class AuthException extends Exception {}

View file

@ -14,6 +14,9 @@ class Notifications
const SocialGoogleLoginAccessDisable = 108;
const DomainNotAllowed = 109;
const AccountNotAllowed = 110;
const AccountTwoFactorAuthRequired = 120;
const AccountTwoFactorAuthError = 121;
const CantGetMessageList = 201;
const CantGetMessage = 202;

View file

@ -0,0 +1,45 @@
<?php
namespace RainLoop\Providers;
class TwoFactorAuth extends \RainLoop\Providers\AbstractProvider
{
/**
* @var \RainLoop\Providers\TwoFactorAuth\TwoFactorAuthInterface
*/
private $oDriver;
/**
* @param \RainLoop\Providers\TwoFactorAuth\TwoFactorAuthInterface|null $oDriver = null
*
* @return void
*/
public function __construct($oDriver = null)
{
$this->oDriver = $oDriver;
}
/**
* @return bool
*/
public function IsActive()
{
return $this->oDriver instanceof \RainLoop\Providers\TwoFactorAuth\TwoFactorAuthInterface;
}
/**
* @param string $sSecret
* @param string $sCode
* @return bool
*/
public function VerifyCode($sSecret, $sCode)
{
$bResult = false;
if ($this->IsActive())
{
$bResult = $this->oDriver->VerifyCode($sSecret, $sCode);
}
return $bResult;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace RainLoop\Providers\TwoFactorAuth;
abstract class AbstractTwoFactorAuth
{
/**
* @return string
*/
public function Label()
{
return 'Two Factor Authenticator Code';
}
/**
* @param string $sSecret
* @param string $sCode
* @return bool
*/
public function VerifyCode($sSecret, $sCode)
{
return false;
}
}

View file

@ -0,0 +1,206 @@
<?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;
}
}

View file

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

View file

@ -113,14 +113,14 @@ class CardDAV implements \Sabre\CardDAV\Backend\BackendInterface
* See Sabre\DAV\IProperties for a description of the mutations array, as
* well as the return value.
*
* @param mixed $mAddressBookId
* @param mixed $mAddressBookID
* @param array $aMutations
* @see Sabre\DAV\IProperties::updateProperties
* @return bool|array
*/
public function updateAddressBook($mAddressBookID, array $aMutations)
{
$this->writeLog('::updateAddressBook('.$mAddressBookID.', $aMutations)');
$this->writeLog('::updateAddressBook('.$mAddressBookID.', array $aMutations['.\count($aMutations).'])');
return false;
}
@ -136,7 +136,7 @@ class CardDAV implements \Sabre\CardDAV\Backend\BackendInterface
*/
public function createAddressBook($sPrincipalUri, $sUrl, array $aProperties)
{
$this->writeLog('::createAddressBook('.$sPrincipalUri.', '.$sUrl.', $aProperties)');
$this->writeLog('::createAddressBook('.$sPrincipalUri.', '.$sUrl.', array $aProperties['.\count($aProperties).'])');
}
/**
@ -181,7 +181,7 @@ class CardDAV implements \Sabre\CardDAV\Backend\BackendInterface
$sEmail = $this->getAuthEmail('', $mAddressBookID);
if (!empty($sEmail))
{
$aList = $this->oPersonalAddressBook->GetContacts($sEmail, 0, 500);
$aList = $this->oPersonalAddressBook->GetContacts($sEmail, 0, 5000);
foreach ($aList as /* @var $oItem \RainLoop\Providers\PersonalAddressBook\Classes\Contact */ $oItem)
{
if (!$oItem->ReadOnly)
@ -235,8 +235,6 @@ class CardDAV implements \Sabre\CardDAV\Backend\BackendInterface
'size' => $oContact->CardDavSize,
'carddata' => $oContact->CardDavData
);
// $this->writeLog($mResult);
}
return $mResult;