mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-11-03 19:08:50 +03:00
Improved user authentication system by supporting OpenSSL aes-256-cbc-hmac-sha1 encryption
This commit is contained in:
parent
99c72ad9e6
commit
8ca043b6e4
16 changed files with 468 additions and 450 deletions
|
|
@ -10,21 +10,20 @@ use \RainLoop\Exceptions\ClientException;
|
|||
|
||||
trait UserAuth
|
||||
{
|
||||
/*
|
||||
const AUTH_SIGN_ME_TOKEN_KEY = 'smremember';
|
||||
const AUTH_SPEC_TOKEN_KEY = 'smspecauth';
|
||||
const AUTH_SPEC_LOGOUT_TOKEN_KEY = 'smspeclogout';
|
||||
const AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY = 'smspeclogoutcmk';
|
||||
*/
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sSpecAuthToken;
|
||||
private $sSpecAuthToken = null;
|
||||
|
||||
/**
|
||||
* Or use 'aes-256-xts' ?
|
||||
*/
|
||||
private static $sCipher = 'aes-256-cbc-hmac-sha1';
|
||||
|
||||
/**
|
||||
* @throws \RainLoop\Exceptions\ClientException
|
||||
*/
|
||||
public function LoginProcess(string &$sEmail, string &$sPassword, string $sSignMeToken = ''): Account
|
||||
public function LoginProcess(string &$sEmail, string &$sPassword, bool $bSignMe = false): Account
|
||||
{
|
||||
$sInputEmail = $sEmail;
|
||||
|
||||
|
|
@ -117,7 +116,7 @@ trait UserAuth
|
|||
$oAccount = null;
|
||||
$sClientCert = \trim($this->Config()->Get('ssl', 'client_cert', ''));
|
||||
try {
|
||||
$oAccount = $this->LoginProvide($sEmail, $sLogin, $sPassword, $sSignMeToken, $sClientCert, true);
|
||||
$oAccount = Account::NewInstanceByLogin($this, $sEmail, $sLogin, $sPassword, $sClientCert, true);
|
||||
|
||||
if (!$oAccount) {
|
||||
throw new ClientException(Notifications::AuthError);
|
||||
|
|
@ -130,6 +129,7 @@ trait UserAuth
|
|||
|
||||
try {
|
||||
$this->CheckMailConnection($oAccount, true);
|
||||
$bSignMe && $this->SetSignMeToken($oAccount);
|
||||
} catch (\Throwable $oException) {
|
||||
$this->loginErrorDelay();
|
||||
|
||||
|
|
@ -142,107 +142,27 @@ trait UserAuth
|
|||
/**
|
||||
* @throws \RainLoop\Exceptions\ClientException
|
||||
*/
|
||||
public function GetAccountFromCustomToken(string $sToken, bool $bThrowExceptionOnFalse = true, bool $bValidateShortToken = true, bool $bQ = false): ?Account
|
||||
public function GetAccountFromCustomToken(string $sToken, bool $bValidateShortToken = true, bool $bQ = false, bool $bThrowExceptionOnFalse = false): ?Account
|
||||
{
|
||||
$oResult = null;
|
||||
if (!empty($sToken)) {
|
||||
$aAccountHash = $bQ ? Utils::DecodeKeyValuesQ($sToken) : Utils::DecodeKeyValues($sToken);
|
||||
if (!empty($aAccountHash[0]) && 'token' === $aAccountHash[0] && // simple token validation
|
||||
8 <= \count($aAccountHash) && // length checking
|
||||
!empty($aAccountHash[7]) && // does short token exist
|
||||
(!$bValidateShortToken || Utils::GetShortToken() === $aAccountHash[7] || // check short token if needed
|
||||
(isset($aAccountHash[10]) && 0 < $aAccountHash[10] && \time() < $aAccountHash[10]))
|
||||
) {
|
||||
$oAccount = $this->LoginProvide($aAccountHash[1], $aAccountHash[2], $aAccountHash[3],
|
||||
empty($aAccountHash[5]) ? '' : $aAccountHash[5], empty($aAccountHash[11]) ? '' : $aAccountHash[11], $bThrowExceptionOnFalse);
|
||||
|
||||
if ($oAccount) {
|
||||
// init proxy user/password
|
||||
if (!empty($aAccountHash[8]) && !empty($aAccountHash[9])) {
|
||||
$oAccount->SetProxyAuthUser($aAccountHash[8]);
|
||||
$oAccount->SetProxyAuthPassword($aAccountHash[9]);
|
||||
}
|
||||
|
||||
$this->Logger()->AddSecret($oAccount->Password());
|
||||
$this->Logger()->AddSecret($oAccount->ProxyAuthPassword());
|
||||
|
||||
$oAccount->SetParentEmail($aAccountHash[6]);
|
||||
$oResult = $oAccount;
|
||||
}
|
||||
} else if ($bThrowExceptionOnFalse) {
|
||||
throw new ClientException(Notifications::AuthError);
|
||||
}
|
||||
$aToken = $bQ ? Utils::DecodeKeyValuesQ($sToken) : Utils::DecodeKeyValues($sToken);
|
||||
return Account::NewInstanceFromTokenArray(
|
||||
$this,
|
||||
$bQ ? Utils::DecodeKeyValuesQ($sToken) : Utils::DecodeKeyValues($sToken),
|
||||
$bThrowExceptionOnFalse
|
||||
);
|
||||
}
|
||||
|
||||
if ($bThrowExceptionOnFalse && !$oResult) {
|
||||
throw new ClientException(Notifications::AuthError);
|
||||
if ($bThrowExceptionOnFalse) {
|
||||
throw new ClientException(\RainLoop\Notifications::AuthError);
|
||||
}
|
||||
|
||||
return $oResult;
|
||||
}
|
||||
|
||||
protected function LoginProvide(string $sEmail, string $sLogin, string $sPassword, string $sSignMeToken = '', string $sClientCert = '', bool $bThrowProvideException = false): ?Account
|
||||
{
|
||||
$oAccount = null;
|
||||
if (\strlen($sEmail) && \strlen($sLogin) && \strlen($sPassword)) {
|
||||
$oDomain = $this->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true);
|
||||
if ($oDomain) {
|
||||
if ($oDomain->ValidateWhiteList($sEmail, $sLogin)) {
|
||||
$oAccount = new Account($sEmail, $sLogin, $sPassword, $oDomain, $sSignMeToken, '', '', $sClientCert);
|
||||
$this->Plugins()->RunHook('filter.account', array($oAccount));
|
||||
|
||||
if ($bThrowProvideException && !$oAccount) {
|
||||
throw new ClientException(Notifications::AuthError);
|
||||
}
|
||||
} else if ($bThrowProvideException) {
|
||||
throw new ClientException(Notifications::AccountNotAllowed);
|
||||
}
|
||||
} else if ($bThrowProvideException) {
|
||||
throw new ClientException(Notifications::DomainNotAllowed);
|
||||
}
|
||||
}
|
||||
|
||||
return $oAccount;
|
||||
}
|
||||
|
||||
public function GetSpecAuthTokenCookie(): string
|
||||
{
|
||||
return Utils::GetCookie(self::AUTH_SPEC_TOKEN_KEY, '');
|
||||
}
|
||||
|
||||
// rlspecauth / AuthAccountHash
|
||||
public function getAuthAccountHash() : string
|
||||
{
|
||||
if ('' === $this->sSpecAuthToken && !\strlen($this->GetSpecAuthLogoutTokenWithDeletion())) {
|
||||
$sAuthAccountHash = $this->GetSpecAuthTokenCookie() ?: $this->GetSpecAuthToken();
|
||||
if (empty($sAuthAccountHash)) {
|
||||
$oAccount = $this->GetAccountFromSignMeToken();
|
||||
if ($oAccount) try
|
||||
{
|
||||
$this->CheckMailConnection($oAccount);
|
||||
$this->AuthToken($oAccount);
|
||||
$sAuthAccountHash = $this->GetSpecAuthToken();
|
||||
}
|
||||
catch (\Throwable $oException)
|
||||
{
|
||||
$oException = null;
|
||||
$this->ClearSignMeData($oAccount);
|
||||
}
|
||||
}
|
||||
$this->SetSpecAuthToken($sAuthAccountHash);
|
||||
}
|
||||
return $this->GetSpecAuthToken();
|
||||
}
|
||||
|
||||
private function getLocalAuthToken(): string
|
||||
{
|
||||
$sToken = $this->GetSpecAuthToken();
|
||||
return !empty($sToken) && '_' === \substr($sToken, 0, 1) ? \substr($sToken, 1) : '';
|
||||
return null;
|
||||
}
|
||||
|
||||
public function GetShortLifeSpecAuthToken(int $iLife = 60): string
|
||||
{
|
||||
$aAccountHash = Utils::DecodeKeyValues($this->getLocalAuthToken());
|
||||
$aAccountHash = Utils::DecodeKeyValues($this->sSpecAuthToken);
|
||||
if (!empty($aAccountHash[0]) && 'token' === $aAccountHash[0]) {
|
||||
$aAccountHash[10] = \time() + $iLife;
|
||||
return Utils::EncodeKeyValues($aAccountHash);
|
||||
|
|
@ -256,126 +176,141 @@ trait UserAuth
|
|||
*/
|
||||
public function getAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Account
|
||||
{
|
||||
return $this->GetAccountFromCustomToken($this->getLocalAuthToken(), $bThrowExceptionOnFalse, true, true);
|
||||
}
|
||||
|
||||
public function GetSpecAuthToken(): string
|
||||
{
|
||||
return $this->sSpecAuthToken;
|
||||
}
|
||||
|
||||
public function SetSpecAuthToken(string $sSpecAuthToken): self
|
||||
{
|
||||
$this->sSpecAuthToken = $sSpecAuthToken;
|
||||
|
||||
return $this;
|
||||
if (!\is_string($this->sSpecAuthToken)) {
|
||||
$this->sSpecAuthToken = '';
|
||||
if (isset($_COOKIE[self::AUTH_SPEC_LOGOUT_TOKEN_KEY])) {
|
||||
Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY);
|
||||
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
|
||||
// Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY);
|
||||
} else {
|
||||
$sAuthAccountHash = Utils::GetCookie(self::AUTH_SPEC_TOKEN_KEY);
|
||||
if (empty($sAuthAccountHash)) {
|
||||
$oAccount = $this->GetAccountFromSignMeToken();
|
||||
if ($oAccount) {
|
||||
$this->SetAuthToken($oAccount);
|
||||
$sAuthAccountHash = $this->sSpecAuthToken;
|
||||
}
|
||||
}
|
||||
$this->sSpecAuthToken = $sAuthAccountHash ?: '';
|
||||
}
|
||||
}
|
||||
return $this->GetAccountFromCustomToken($this->sSpecAuthToken, true, true, $bThrowExceptionOnFalse);
|
||||
}
|
||||
|
||||
public function SetAuthToken(Account $oAccount): void
|
||||
{
|
||||
$sSpecAuthToken = '_' . $oAccount->GetAuthTokenQ();
|
||||
$sSpecAuthToken = $oAccount->GetAuthTokenQ();
|
||||
|
||||
$this->SetSpecAuthToken($sSpecAuthToken);
|
||||
$this->sSpecAuthToken = $sSpecAuthToken;
|
||||
Utils::SetCookie(self::AUTH_SPEC_TOKEN_KEY, $sSpecAuthToken);
|
||||
|
||||
if ($oAccount->SignMe()) {
|
||||
$this->SetSignMeToken($oAccount);
|
||||
if (isset($aAccounts[$oAccount->Email()])) {
|
||||
$aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken();
|
||||
$this->SetAccounts($oAccount, $aAccounts);
|
||||
}
|
||||
}
|
||||
|
||||
private static function GetSignMeToken(): ?array
|
||||
{
|
||||
$sSignMeToken = Utils::GetCookie(self::AUTH_SIGN_ME_TOKEN_KEY, '');
|
||||
return empty($sSignMeToken) ? null : Utils::DecodeKeyValuesQ($sSignMeToken);
|
||||
$sSignMeToken = Utils::GetCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
|
||||
if (empty($sSignMeToken)) {
|
||||
return null;
|
||||
}
|
||||
$aResult = \SnappyMail\Crypt::DecryptCookie($sSignMeToken);
|
||||
return \is_array($aResult) ? $aResult : null;
|
||||
}
|
||||
|
||||
private static function SetSignMeTokenCookie(array $aData): void
|
||||
{
|
||||
Utils::SetCookie(
|
||||
self::AUTH_SIGN_ME_TOKEN_KEY,
|
||||
\SnappyMail\Crypt::EncryptCookie($aData),
|
||||
\time() + 3600 * 24 * 30 // 30 days
|
||||
);
|
||||
}
|
||||
|
||||
private function SetSignMeToken(Account $oAccount): void
|
||||
{
|
||||
$this->ClearSignMeData($oAccount);
|
||||
$this->ClearSignMeData();
|
||||
|
||||
$uuid = \SnappyMail\UUID::generate();
|
||||
$salt = \sha1(\random_bytes(16));
|
||||
Utils::SetCookie(self::AUTH_SIGN_ME_TOKEN_KEY,
|
||||
Utils::EncodeKeyValuesQ(array(
|
||||
$data = \SnappyMail\Crypt::EncryptRaw($oAccount);
|
||||
|
||||
if ($data['xxtea']) {
|
||||
static::SetSignMeTokenCookie(array(
|
||||
'e' => $oAccount->Email(),
|
||||
'u' => $uuid,
|
||||
's' => $salt
|
||||
)),
|
||||
\time() + 3600 * 24 * 30); // 30 days
|
||||
's' => \base64_encode($data[1])
|
||||
));
|
||||
} else {
|
||||
static::SetSignMeTokenCookie(array(
|
||||
'e' => $oAccount->Email(),
|
||||
'u' => $uuid,
|
||||
'i' => \base64_encode($data[1])
|
||||
));
|
||||
}
|
||||
|
||||
$this->StorageProvider()->Put($oAccount,
|
||||
$this->StorageProvider()->Put(
|
||||
$oAccount,
|
||||
StorageType::SIGN_ME,
|
||||
$uuid,
|
||||
Utils::EncryptString($oAccount->GetAuthToken(), $salt)
|
||||
$data[2]
|
||||
);
|
||||
}
|
||||
|
||||
public function GetAccountFromSignMeToken(): ?Account
|
||||
{
|
||||
$oAccount = null;
|
||||
|
||||
$aTokenData = static::GetSignMeToken();
|
||||
if (!empty($aTokenData)) {
|
||||
if (!empty($aTokenData['e']) && !empty($aTokenData['u']) && !empty($aTokenData['s']) && \SnappyMail\UUID::isValid($aTokenData['u'])) {
|
||||
$sAuthToken = $this->StorageProvider()->Get($aTokenData['e'],
|
||||
$oAccount = null;
|
||||
if (!empty($aTokenData['e']) && !empty($aTokenData['u']) && \SnappyMail\UUID::isValid($aTokenData['u'])) {
|
||||
$sAuthToken = $this->StorageProvider()->Get(
|
||||
$aTokenData['e'],
|
||||
StorageType::SIGN_ME,
|
||||
$aTokenData['u']
|
||||
);
|
||||
if (empty($sAuthToken)) {
|
||||
return null;
|
||||
}
|
||||
$sAuthToken = Utils::DecryptString($sAuthToken, $aTokenData['s']);
|
||||
if (!empty($sAuthToken)) {
|
||||
$oAccount = $this->GetAccountFromCustomToken($sAuthToken, false, false, true);
|
||||
if (!empty($aTokenData['s'])) {
|
||||
$aAccountHash = \SnappyMail\Crypt::XxteaDecrypt($sAuthToken, \base64_decode($aTokenData['s']));
|
||||
} else if (!empty($aTokenData['i'])) {
|
||||
$aAccountHash = \SnappyMail\Crypt::OpenSSLDecrypt($sAuthToken, \base64_decode($aTokenData['i']));
|
||||
}
|
||||
} else if (!empty($aTokenData['e']) && !empty($aTokenData['t'])) {
|
||||
// This is old, see https://github.com/the-djmaze/snappymail/issues/126
|
||||
$sTokenSettings = $this->StorageProvider()->Get($aTokenData['e'],
|
||||
StorageType::CONFIG,
|
||||
'sign_me'
|
||||
);
|
||||
if (!empty($sTokenSettings)) {
|
||||
$aSignMeData = Utils::DecodeKeyValuesQ($sTokenSettings);
|
||||
if (!empty($aSignMeData['AuthToken']) &&
|
||||
!empty($aSignMeData['SignMeToken']) &&
|
||||
$aSignMeData['SignMeToken'] === $aTokenData['t']) {
|
||||
$oAccount = $this->GetAccountFromCustomToken($aSignMeData['AuthToken'], false, false, true);
|
||||
if (!empty($aAccountHash) && \is_array($aAccountHash)) {
|
||||
$oAccount = Account::NewInstanceFromTokenArray($this, $aAccountHash);
|
||||
if ($oAccount) {
|
||||
try
|
||||
{
|
||||
$this->CheckMailConnection($oAccount);
|
||||
// Update lifetime
|
||||
$this->SetSignMeToken($oAccount);
|
||||
|
||||
return $oAccount;
|
||||
}
|
||||
catch (\Throwable $oException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($oAccount) {
|
||||
// Update lifetime
|
||||
$this->SetSignMeToken($oAccount);
|
||||
}
|
||||
} else {
|
||||
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
|
||||
}
|
||||
|
||||
return $oAccount;
|
||||
$this->ClearSignMeData();
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function ClearSignMeData(Account $oAccount) : void
|
||||
protected function ClearSignMeData() : void
|
||||
{
|
||||
if ($oAccount) {
|
||||
if (isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY])) {
|
||||
$aTokenData = static::GetSignMeToken();
|
||||
if (!empty($aTokenData['u']) && \SnappyMail\UUID::isValid($aTokenData['u'])) {
|
||||
$this->StorageProvider()->Clear($oAccount, StorageType::SIGN_ME, $aTokenData['u']);
|
||||
if (!empty($aTokenData['e']) && !empty($aTokenData['u']) && \SnappyMail\UUID::isValid($aTokenData['u'])) {
|
||||
$this->StorageProvider()->Clear($aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u']);
|
||||
}
|
||||
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
|
||||
$this->StorageProvider()->Clear($oAccount, StorageType::CONFIG, 'sign_me');
|
||||
}
|
||||
}
|
||||
|
||||
public function GetSpecAuthLogoutTokenWithDeletion(): string
|
||||
{
|
||||
$sResult = Utils::GetCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY, '');
|
||||
if (\strlen($sResult)) {
|
||||
Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY);
|
||||
}
|
||||
|
||||
return $sResult;
|
||||
}
|
||||
|
||||
public function SetAuthLogoutToken(): void
|
||||
{
|
||||
\header('X-RainLoop-Action: Logout');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue