271 lines
6.6 KiB
PHP
271 lines
6.6 KiB
PHP
<?php
|
|
/**
|
|
* Copyright 2015-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (LGPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
|
|
*
|
|
* @category Horde
|
|
* @copyright 2015-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Imap_Client
|
|
*/
|
|
|
|
/**
|
|
* Provides authentication via the SCRAM SASL mechanism (RFC 5802 [3]).
|
|
*
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @category Horde
|
|
* @copyright 2015-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Imap_Client
|
|
* @since 2.29.0
|
|
*/
|
|
class Horde_Imap_Client_Auth_Scram
|
|
{
|
|
/**
|
|
* AuthMessage (RFC 5802 [3]).
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_authmsg;
|
|
|
|
/**
|
|
* Hash name.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_hash;
|
|
|
|
/**
|
|
* Number of Hi iterations (RFC 5802 [2]).
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $_iterations;
|
|
|
|
/**
|
|
* Nonce.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_nonce;
|
|
|
|
/**
|
|
* Password.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_pass;
|
|
|
|
/**
|
|
* Server salt.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_salt;
|
|
|
|
/**
|
|
* Calculated server signature value.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_serversig;
|
|
|
|
/**
|
|
* Username.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_user;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param string $user Username.
|
|
* @param string $pass Password.
|
|
* @param string $hash Hash name.
|
|
*
|
|
* @throws Horde_Imap_Client_Exception
|
|
*/
|
|
public function __construct($user, $pass, $hash = 'SHA1')
|
|
{
|
|
$error = false;
|
|
|
|
$this->_hash = $hash;
|
|
|
|
try {
|
|
if (!class_exists('Horde_Stringprep') ||
|
|
!class_exists('Horde_Crypt_Blowfish_Pbkdf2')) {
|
|
throw new Exception();
|
|
}
|
|
|
|
Horde_Stringprep::autoload();
|
|
$saslprep = new Znerol\Component\Stringprep\Profile\SASLprep();
|
|
|
|
$this->_user = $saslprep->apply(
|
|
$user,
|
|
'UTF-8',
|
|
Znerol\Component\Stringprep\Profile::MODE_QUERY
|
|
);
|
|
$this->_pass = $saslprep->apply(
|
|
$pass,
|
|
'UTF-8',
|
|
Znerol\Component\Stringprep\Profile::MODE_STORE
|
|
);
|
|
} catch (Znerol\Component\Stringprep\ProfileException $e) {
|
|
$error = true;
|
|
} catch (Exception $e) {
|
|
$error = true;
|
|
}
|
|
|
|
if ($error) {
|
|
throw new Horde_Imap_Client_Exception(
|
|
Horde_Imap_Client_Translation::r("Authentication failure."),
|
|
Horde_Imap_Client_Exception::LOGIN_AUTHORIZATIONFAILED
|
|
);
|
|
}
|
|
|
|
/* Generate nonce. (Done here so this can be overwritten for
|
|
* testing purposes.) */
|
|
$this->_nonce = strval(new Horde_Support_Randomid());
|
|
}
|
|
|
|
/**
|
|
* Return the initial client message.
|
|
*
|
|
* @return string Initial client message.
|
|
*/
|
|
public function getClientFirstMessage()
|
|
{
|
|
/* n: client doesn't support channel binding,
|
|
* <empty>,
|
|
* n=<user>: SASLprepped username with "," and "=" escaped,
|
|
* r=<nonce>: Random nonce */
|
|
$this->_authmsg = 'n=' . str_replace(
|
|
array(',', '='),
|
|
array('=2C', '=3D'),
|
|
$this->_user
|
|
) . ',r=' . $this->_nonce;
|
|
|
|
return 'n,,' . $this->_authmsg;
|
|
}
|
|
|
|
/**
|
|
* Process the initial server message response.
|
|
*
|
|
* @param string $msg Initial server response.
|
|
*
|
|
* @return boolean False if authentication failed at this stage.
|
|
*/
|
|
public function parseServerFirstMessage($msg)
|
|
{
|
|
$i = $r = $s = false;
|
|
|
|
foreach (explode(',', $msg) as $val) {
|
|
list($attr, $aval) = array_map('trim', explode('=', $val, 2));
|
|
|
|
switch ($attr) {
|
|
case 'i':
|
|
$this->_iterations = intval($aval);
|
|
$i = true;
|
|
break;
|
|
|
|
case 'r':
|
|
/* Beginning of server-provided nonce MUST be the same as the
|
|
* nonce we provided. */
|
|
if (strpos($aval, $this->_nonce) !== 0) {
|
|
return false;
|
|
}
|
|
$this->_nonce = $aval;
|
|
$r = true;
|
|
break;
|
|
|
|
case 's':
|
|
$this->_salt = base64_decode($aval);
|
|
$s = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($i && $r && $s) {
|
|
$this->_authmsg .= ',' . $msg;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return the final client message.
|
|
*
|
|
* @return string Final client message.
|
|
*/
|
|
public function getClientFinalMessage()
|
|
{
|
|
$final_msg = 'c=biws,r=' . $this->_nonce;
|
|
|
|
/* Salted password. */
|
|
$s_pass = strval(new Horde_Crypt_Blowfish_Pbkdf2(
|
|
$this->_pass,
|
|
strlen(hash($this->_hash, '', true)),
|
|
array(
|
|
'algo' => $this->_hash,
|
|
'i_count' => $this->_iterations,
|
|
'salt' => $this->_salt
|
|
)
|
|
));
|
|
|
|
/* Client key. */
|
|
$c_key = hash_hmac($this->_hash, 'Client Key', $s_pass, true);
|
|
|
|
/* Stored key. */
|
|
$s_key = hash($this->_hash, $c_key, true);
|
|
|
|
/* Client signature. */
|
|
$auth_msg = $this->_authmsg . ',' . $final_msg;
|
|
$c_sig = hash_hmac($this->_hash, $auth_msg, $s_key, true);
|
|
|
|
/* Proof. */
|
|
$proof = $c_key ^ $c_sig;
|
|
|
|
/* Server signature. */
|
|
$this->_serversig = hash_hmac(
|
|
$this->_hash,
|
|
$auth_msg,
|
|
hash_hmac($this->_hash, 'Server Key', $s_pass, true),
|
|
true
|
|
);
|
|
|
|
/* c=biws: channel-binding ("biws" = base64('n,,')),
|
|
* p=<proof>: base64 encoded ClientProof,
|
|
* r=<nonce>: Nonce as returned from the server. */
|
|
return $final_msg . ',p=' . base64_encode($proof);
|
|
}
|
|
|
|
/**
|
|
* Process the final server message response.
|
|
*
|
|
* @param string $msg Final server response.
|
|
*
|
|
* @return boolean False if authentication failed.
|
|
*/
|
|
public function parseServerFinalMessage($msg)
|
|
{
|
|
foreach (explode(',', $msg) as $val) {
|
|
list($attr, $aval) = array_map('trim', explode('=', $val, 2));
|
|
|
|
switch ($attr) {
|
|
case 'e':
|
|
return false;
|
|
|
|
case 'v':
|
|
return (base64_decode($aval) === $this->_serversig);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
}
|