mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-04 06:57:03 +03:00
Two Factor Authentication (first look)
knockoutjs 3.1.0 New icons (archive, buy, filter) Many small fixes.
This commit is contained in:
parent
32aedce2a0
commit
a158164e80
24 changed files with 917 additions and 420 deletions
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace RainLoop\Exceptions;
|
||||
|
||||
/**
|
||||
* @category RainLoop
|
||||
* @package Exceptions
|
||||
*/
|
||||
class AuthException extends Exception {}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace RainLoop\Providers\TwoFactorAuth;
|
||||
|
||||
interface TwoFactorAuthInterface
|
||||
{
|
||||
/**
|
||||
* @param string $sSecret
|
||||
* @param string $sCode
|
||||
* @return bool
|
||||
*/
|
||||
public function VerifyCode($sSecret, $sCode);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue