mirror of
https://github.com/the-djmaze/snappymail.git
synced 2025-09-11 15:44:43 +08:00
Added support for SCRAM-SHA-1 & SCRAM-SHA-256
https://github.com/RainLoop/rainloop-webmail/issues/1914
This commit is contained in:
parent
d0dc257619
commit
68d103b3c1
6 changed files with 330 additions and 118 deletions
|
@ -62,11 +62,6 @@ class SmtpClient extends \MailSo\Net\NetClient
|
|||
*/
|
||||
private $aResults;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $__USE_SINGLE_LINE_AUTH_PLAIN_COMMAND = false;
|
||||
|
||||
function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
@ -150,126 +145,94 @@ class SmtpClient extends \MailSo\Net\NetClient
|
|||
{
|
||||
$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sLogin));
|
||||
|
||||
if ($bUseAuthCramMd5IfSupported && $this->IsAuthSupported('CRAM-MD5'))
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->sendRequestWithCheck('AUTH', 334, 'CRAM-MD5');
|
||||
}
|
||||
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
|
||||
{
|
||||
$this->writeLogException(
|
||||
new \MailSo\Smtp\Exceptions\LoginBadMethodException(
|
||||
$oException->GetResponses(), $oException->getMessage(), 0, $oException),
|
||||
\MailSo\Log\Enumerations\Type::NOTICE, true);
|
||||
}
|
||||
|
||||
$sTicket = '';
|
||||
|
||||
$sContinuationResponse = !empty($this->aResults[0]) ? \trim($this->aResults[0]) : '';
|
||||
if ($sContinuationResponse && '334 ' === \substr($sContinuationResponse, 0, 4) && 0 < \strlen(\substr($sContinuationResponse, 4)))
|
||||
{
|
||||
$sTicket = \base64_decode(\substr($sContinuationResponse, 4));
|
||||
$this->writeLogWithCrlf('ticket: '.$sTicket);
|
||||
}
|
||||
|
||||
if (empty($sTicket))
|
||||
{
|
||||
$this->writeLogException(
|
||||
new \MailSo\Smtp\Exceptions\NegativeResponseException,
|
||||
\MailSo\Log\Enumerations\Type::NOTICE, true
|
||||
);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->sendRequestWithCheck(\base64_encode($sLogin.' '.\hash_hmac('md5', $sTicket, $sPassword)), 235, '', true);
|
||||
}
|
||||
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
|
||||
{
|
||||
$this->writeLogException(
|
||||
new \MailSo\Smtp\Exceptions\LoginBadCredentialsException(
|
||||
$oException->GetResponses(), $oException->getMessage(), 0, $oException),
|
||||
\MailSo\Log\Enumerations\Type::NOTICE, true);
|
||||
// $encrypted = !empty(\stream_get_meta_data($this->rConnect)['crypto']);
|
||||
$type = '';
|
||||
$types = [
|
||||
'SCRAM-SHA-256' => 1, // !$encrypted
|
||||
'SCRAM-SHA-1' => 1, // !$encrypted
|
||||
'CRAM-MD5' => $bUseAuthCramMd5IfSupported,
|
||||
'PLAIN' => $bUseAuthPlainIfSupported,
|
||||
'LOGIN' => 1 // $encrypted
|
||||
];
|
||||
foreach ($types as $sasl_type => $active) {
|
||||
if ($active && $this->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) {
|
||||
$type = $sasl_type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if ($bUseAuthPlainIfSupported && $this->IsAuthSupported('PLAIN'))
|
||||
{
|
||||
if ($this->__USE_SINGLE_LINE_AUTH_PLAIN_COMMAND)
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->sendRequestWithCheck('AUTH', 235, 'PLAIN '.\base64_encode("\0".$sLogin."\0".$sPassword), true);
|
||||
}
|
||||
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
|
||||
{
|
||||
$this->writeLogException(
|
||||
new \MailSo\Smtp\Exceptions\LoginBadCredentialsException(
|
||||
$oException->GetResponses(), $oException->getMessage(), 0, $oException),
|
||||
\MailSo\Log\Enumerations\Type::NOTICE, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->sendRequestWithCheck('AUTH', 334, 'PLAIN');
|
||||
}
|
||||
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
|
||||
{
|
||||
$this->writeLogException(
|
||||
new \MailSo\Smtp\Exceptions\LoginBadMethodException(
|
||||
$oException->GetResponses(), $oException->getMessage(), 0, $oException),
|
||||
\MailSo\Log\Enumerations\Type::NOTICE, true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->sendRequestWithCheck(\base64_encode("\0".$sLogin."\0".$sPassword), 235, '', true);
|
||||
}
|
||||
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
|
||||
{
|
||||
$this->writeLogException(
|
||||
new \MailSo\Smtp\Exceptions\LoginBadCredentialsException(
|
||||
$oException->GetResponses(), $oException->getMessage(), 0, $oException),
|
||||
\MailSo\Log\Enumerations\Type::NOTICE, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ($this->IsAuthSupported('LOGIN'))
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->sendRequestWithCheck('AUTH', 334, 'LOGIN');
|
||||
}
|
||||
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
|
||||
{
|
||||
$this->writeLogException(
|
||||
new \MailSo\Smtp\Exceptions\LoginBadMethodException(
|
||||
$oException->GetResponses(), $oException->getMessage(), 0, $oException),
|
||||
\MailSo\Log\Enumerations\Type::NOTICE, true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->sendRequestWithCheck(\base64_encode($sLogin), 334, '');
|
||||
$this->sendRequestWithCheck(\base64_encode($sPassword), 235, '', true);
|
||||
}
|
||||
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
|
||||
{
|
||||
$this->writeLogException(
|
||||
new \MailSo\Smtp\Exceptions\LoginBadCredentialsException(
|
||||
$oException->GetResponses(), $oException->getMessage(), 0, $oException),
|
||||
\MailSo\Log\Enumerations\Type::NOTICE, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!$type) {
|
||||
$this->writeLogException(
|
||||
new \MailSo\Smtp\Exceptions\LoginBadMethodException,
|
||||
\MailSo\Log\Enumerations\Type::NOTICE, true);
|
||||
}
|
||||
|
||||
$SASL = \SnappyMail\SASL::factory($type);
|
||||
$SASL->base64 = true;
|
||||
|
||||
// Start authentication
|
||||
try
|
||||
{
|
||||
$sResult = $this->sendRequestWithCheck('AUTH', 334, $type);
|
||||
}
|
||||
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
|
||||
{
|
||||
$this->writeLogException(
|
||||
new \MailSo\Smtp\Exceptions\LoginBadMethodException(
|
||||
$oException->GetResponses(), $oException->getMessage(), 0, $oException),
|
||||
\MailSo\Log\Enumerations\Type::NOTICE, true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch ($type)
|
||||
{
|
||||
// RFC 4616
|
||||
case 'PLAIN':
|
||||
$this->sendRequestWithCheck($SASL->authenticate($username, $passphrase), 235, '', true);
|
||||
break;
|
||||
|
||||
case 'LOGIN':
|
||||
$sResult = $this->sendRequestWithCheck($SASL->authenticate($username, $passphrase, $sResult), 334, '');
|
||||
$this->sendRequestWithCheck($SASL->challenge($sResult), 235, '', true);
|
||||
break;
|
||||
|
||||
// RFC 2195
|
||||
case 'CRAM-MD5':
|
||||
if (empty($sResult)) {
|
||||
$this->writeLogException(
|
||||
new \MailSo\Smtp\Exceptions\NegativeResponseException,
|
||||
\MailSo\Log\Enumerations\Type::NOTICE, true
|
||||
);
|
||||
}
|
||||
$this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 235, '', true);
|
||||
break;
|
||||
|
||||
// RFC 5802
|
||||
case 'SCRAM-SHA-1':
|
||||
case 'SCRAM-SHA-256':
|
||||
$sResult = $this->sendRequestWithCheck($SASL->authenticate($username, $passphrase, $sResult), 234, '');
|
||||
$sResult = $this->sendRequestWithCheck($SASL->challenge($sResult), 235, '', true);
|
||||
$SASL->verify($sResult);
|
||||
break;
|
||||
|
||||
/*
|
||||
// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||
case 'XOAUTH2':
|
||||
throw new \Exception('Please use app passphrases: https://support.google.com/mail/answer/185833');
|
||||
break;
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
||||
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
|
||||
{
|
||||
$this->writeLogException(
|
||||
new \MailSo\Smtp\Exceptions\LoginBadCredentialsException(
|
||||
$oException->GetResponses(), $oException->getMessage(), 0, $oException),
|
||||
\MailSo\Log\Enumerations\Type::NOTICE, true);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -542,10 +505,11 @@ class SmtpClient extends \MailSo\Net\NetClient
|
|||
* @throws \MailSo\Net\Exceptions\Exception
|
||||
* @throws \MailSo\Smtp\Exceptions\Exception
|
||||
*/
|
||||
private function sendRequestWithCheck(string $sCommand, $mExpectCode, string $sAddToCommand = '', bool $bSecureLog = false, string $sErrorPrefix = '') : void
|
||||
private function sendRequestWithCheck(string $sCommand, $mExpectCode, string $sAddToCommand = '', bool $bSecureLog = false, string $sErrorPrefix = '') : string
|
||||
{
|
||||
$this->sendRequest($sCommand, $sAddToCommand, $bSecureLog);
|
||||
$this->validateResponse($mExpectCode, $sErrorPrefix);
|
||||
return empty($this->aResults[0]) ? '' : \substr($this->aResults[0], 4);
|
||||
}
|
||||
|
||||
private function ehloOrHelo(string $sHost) : void
|
||||
|
|
52
snappymail/v/0.0.0/app/libraries/snappymail/sasl.php
Normal file
52
snappymail/v/0.0.0/app/libraries/snappymail/sasl.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace SnappyMail;
|
||||
|
||||
abstract class SASL
|
||||
{
|
||||
public
|
||||
$base64 = false;
|
||||
|
||||
abstract public function authenticate(string $authcid, string $passphrase, ?string $authzid = null);
|
||||
|
||||
public function challenge(string $challenge) : string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function verify(string $data) : bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
final public static function factory(string $type) : void
|
||||
{
|
||||
if (\preg_match('/^([A-Z2]+)(?:-(.+))?$/Di', $type, $m)) {
|
||||
$class = __CLASS__ . "\\{$m[1]}";
|
||||
if (\class_exists($class)) {
|
||||
return new $class($m[2] ?? '');
|
||||
}
|
||||
}
|
||||
throw new \Exception("Unsupported SASL mechanism type: {$type}");
|
||||
}
|
||||
|
||||
public static function isSupported(string $type) : bool
|
||||
{
|
||||
if (\preg_match('/^([A-Z2]+)(?:-(.+))?$/Di', $type, $m)) {
|
||||
$class = __CLASS__ . "\\{$m[1]}";
|
||||
return \class_exists($class) && $class::isSupported($m[2] ?? '');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final protected function decode(string $data) : string
|
||||
{
|
||||
return $this->base64 ? \base64_decode($data) : $data;
|
||||
}
|
||||
|
||||
final protected function encode(string $data) : string
|
||||
{
|
||||
return $this->base64 ? \base64_encode($data) : $data;
|
||||
}
|
||||
|
||||
}
|
27
snappymail/v/0.0.0/app/libraries/snappymail/sasl/cram.php
Normal file
27
snappymail/v/0.0.0/app/libraries/snappymail/sasl/cram.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace SnappyMail\SASL;
|
||||
|
||||
class Cram extends \SnappyMail\SASL
|
||||
{
|
||||
|
||||
function __construct(string $algo)
|
||||
{
|
||||
$algo = \strtolower($algo);
|
||||
if (!\in_array($algo, \hash_algos())) {
|
||||
throw new \Exception("Unsupported SASL CRAM algorithm: {$algo}");
|
||||
}
|
||||
$this->algo = $algo;
|
||||
}
|
||||
|
||||
public function authenticate(string $authcid, string $passphrase, ?string $challenge = null) : string
|
||||
{
|
||||
return $this->encode($authcid . ' ' . \hash_hmac($this->algo, $this->decode($challenge), $passphrase));
|
||||
}
|
||||
|
||||
public static function isSupported(string $param) : bool
|
||||
{
|
||||
return \in_array(\strtolower($param), \hash_algos());
|
||||
}
|
||||
|
||||
}
|
32
snappymail/v/0.0.0/app/libraries/snappymail/sasl/login.php
Normal file
32
snappymail/v/0.0.0/app/libraries/snappymail/sasl/login.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace SnappyMail\SASL;
|
||||
|
||||
class Login extends \SnappyMail\SASL
|
||||
{
|
||||
protected
|
||||
$passphrase;
|
||||
|
||||
public function authenticate(string $username, string $passphrase, ?string $challenge = null) : string
|
||||
{
|
||||
if ($challenge && 'Username:' !== $this->decode($challenge)) {
|
||||
throw new \Exception("Invalid response: {$challenge}");
|
||||
}
|
||||
$this->passphrase = $passphrase;
|
||||
return $this->encode($username);
|
||||
}
|
||||
|
||||
public function challenge(string $challenge) : string
|
||||
{
|
||||
if ($challenge && 'Password:' !== $this->decode($challenge)) {
|
||||
throw new \Exception("invalid response: {$challenge}");
|
||||
}
|
||||
return $this->encode($this->passphrase);
|
||||
}
|
||||
|
||||
public static function isSupported(string $param) : bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
18
snappymail/v/0.0.0/app/libraries/snappymail/sasl/plain.php
Normal file
18
snappymail/v/0.0.0/app/libraries/snappymail/sasl/plain.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace SnappyMail\SASL;
|
||||
|
||||
class Plain extends \SnappyMail\SASL
|
||||
{
|
||||
|
||||
public function authenticate(string $username, string $passphrase, ?string $authzid = null) : string
|
||||
{
|
||||
return $this->encode("{$authzid}\x00{$username}\x00{$passphrase}");
|
||||
}
|
||||
|
||||
public static function isSupported(string $param) : bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
119
snappymail/v/0.0.0/app/libraries/snappymail/sasl/scram.php
Normal file
119
snappymail/v/0.0.0/app/libraries/snappymail/sasl/scram.php
Normal file
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
/**
|
||||
* https://tools.ietf.org/html/rfc5802
|
||||
* https://tools.ietf.org/html/rfc7677
|
||||
*/
|
||||
|
||||
|
||||
namespace SnappyMail\SASL;
|
||||
|
||||
class Scram extends \SnappyMail\SASL
|
||||
{
|
||||
|
||||
protected
|
||||
$algo,
|
||||
$nonce,
|
||||
$passphrase,
|
||||
$gs2_header,
|
||||
$auth_message,
|
||||
$server_key;
|
||||
|
||||
function __construct(string $algo)
|
||||
{
|
||||
if (\stripos($algo, '-PLUS')) {
|
||||
throw new \Exception("SASL SCRAM channel binding unsupported: {$algo}");
|
||||
}
|
||||
$algo = \str_replace('sha-', 'sha', \strtolower($algo));
|
||||
if (!\in_array($algo, \hash_algos())) {
|
||||
throw new \Exception("SASL SCRAM unsupported algorithm: {$algo}");
|
||||
}
|
||||
$this->algo = $algo;
|
||||
}
|
||||
|
||||
public function authenticate(string $authcid, string $passphrase, ?string $authzid = null) : string
|
||||
{
|
||||
// SASLprep
|
||||
$authcid = \str_replace(array('=',','), array('=3D','=2C'), $authcid);
|
||||
|
||||
$this->nonce = \bin2hex(\random_bytes(16));
|
||||
$this->passphrase = $passphrase;
|
||||
$this->gs2_header = 'n,' . (empty($authzid) ? '' : 'a=' . $authzid) . ',';
|
||||
$this->auth_message = "n={$authcid},r={$this->nonce}";
|
||||
return $this->encode($this->gs2_header . $this->auth_message);
|
||||
}
|
||||
|
||||
public function challenge(string $challenge) : string
|
||||
{
|
||||
$challenge = $this->decode($challenge);
|
||||
$values = static::parseMessage($challenge);
|
||||
|
||||
if (empty($values['r'])) {
|
||||
throw new \Exception('Server nonce not found');
|
||||
}
|
||||
if (empty($values['s'])) {
|
||||
throw new \Exception('Server salt not found');
|
||||
}
|
||||
if (empty($values['i'])) {
|
||||
throw new \Exception('Server iterator not found');
|
||||
}
|
||||
|
||||
if (\substr($values['r'], 0, \strlen($this->nonce)) !== $this->nonce) {
|
||||
throw new \Exception('Server invalid nonce');
|
||||
}
|
||||
|
||||
$salt = \base64_decode($values['s']);
|
||||
if (!$salt) {
|
||||
throw new \Exception('Server invalid salt');
|
||||
}
|
||||
|
||||
$pass = \hash_pbkdf2($this->algo, $this->passphrase, $salt, \intval($values['i']), 0, true);
|
||||
$this->passphrase = null;
|
||||
|
||||
$ckey = \hash_hmac($this->algo, 'Client Key', $pass, true);
|
||||
$skey = \hash($this->algo, $ckey, true);
|
||||
|
||||
$cfmb = 'c='.\base64_encode($this->gs2_header).',r='.$values['r'];
|
||||
$amsg = "{$this->auth_message},{$challenge},{$cfmb}";
|
||||
|
||||
$csig = \hash_hmac($this->algo, $amsg, $skey, true);
|
||||
$proof = \base64_encode($ckey ^ $csig);
|
||||
|
||||
$skey = \hash_hmac($this->algo, 'Server Key', $pass, true);
|
||||
$this->server_key = \hash_hmac($this->algo, $amsg, $skey, true);
|
||||
|
||||
return $this->encode("{$cfmb},p={$proof}");
|
||||
}
|
||||
|
||||
public function verify(string $data) : bool
|
||||
{
|
||||
$v = static::parseMessage($this->decode($data));
|
||||
if (empty($v['v'])) {
|
||||
throw new \Exception('Server signature not found');
|
||||
}
|
||||
if (\base64_encode($this->server_key) !== $v['v']) {
|
||||
throw new \Exception('Server signature invalid');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function parseMessage(string $msg) : array
|
||||
{
|
||||
if ($msg && \preg_match_all('#(\w+)\=(?:"([^"]+)"|([^,]+))#', $msg, $m)) {
|
||||
return \array_combine(
|
||||
$m[1],
|
||||
\array_replace(
|
||||
\array_filter($m[2]),
|
||||
\array_filter($m[3])
|
||||
)
|
||||
);
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
public static function isSupported(string $param) : bool
|
||||
{
|
||||
$param = \str_replace('sha-', 'sha', \strtolower($param));
|
||||
return \in_array($param, \hash_algos());
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue