Skip to content

Instantly share code, notes, and snippets.

@justincjahn
Created October 19, 2011 19:43
Show Gist options
  • Save justincjahn/1299443 to your computer and use it in GitHub Desktop.
Save justincjahn/1299443 to your computer and use it in GitHub Desktop.
SCRAM-SHA256-JSON
<?php
/**
* The index page for a SCRAM-SHA256-JSON implementation.
*
* NOTE: PHP >= 5.2 and PHP-JSON is required.
*
* @author Justin "4sak3n 0ne" Jahn
* @copyright (c) 2011 4sak3n Design (http://www.4sk.us/)
* @license http://www.opensource.org/licenses/bsd-license.php New BSD License
*/
// Include the classes this file will utilize
require_once('ScramPassword.php');
require_once('ScramAuth.php');
// The username and password to use when authenticating
// NOTE: The password here is statically set because it would change on every
/// page load otherwise, which wouldn't work for SCRAM's two phase authentication.
//// $sPassword = hash('sha256', 'testing');
//// $oPassword = new ScramPassword($sPassword);
$sUsername = 'test';
$sPassword = hash('sha256', 'testing');
$oPassword = new ScramPassword($sPassword, array(
'salt' => 'StaticSalt',
'iterations' => 100,
'algorithm' => 'sha256'
));
// If we didn't post anything, then display a login page
if ($_SERVER['REQUEST_METHOD'] != 'POST'):
?>
<!DOCTYPE html>
<html>
<head>
<title>SCRAM Tutorial</title>
<script src="http://ajax.googleapis.com/ajax/libs/dojo/1.6.1/dojo/dojo.xd.js"
type="text/javascript"></script>
<style type="text/css" media="screen">
p.error, p.success {
padding: 0.5em;
margin: 0.75em;
}
p.error {
color: #A31A28;
background-color: #EDAFBF;
border: 1px solid #D61E27;
}
p.success {
color: #196322;
background-color: #A9F2A5;
border: 1px solid #6DA86A;
}
</style>
</head>
<body>
<form id="login" action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
<label for="username">Username: </label>
<input type="text" id="username" name="username" />
<label for="password">Password: </label>
<input type="password" id="password" name="password" />
<input type="submit" name="loginSubmit" value="Login" />
</form>
<script src="scram.js" type="text/javascript"></script>
</body>
</html>
<?php
else:
// Something has been POSTED, verfy it's JSON
$sContent = isset($_SERVER['CONTENT_TYPE'])
? $_SERVER['CONTENT_TYPE']
: null;
if (strpos(strtolower($sContent), 'application/json') === false) {
die('Invalid Request');
}
// It's valid, perform the SCRAM phases
$oScram = new ScramAuth($sUsername, $oPassword);
$iReturn = $oScram->authenticate();
$sReturn = $oScram->getResponse();
// The StormAuth class provides us several return values that we may hook custom
/// functionality into. For the most part, if it's a JSON request, it is expecting
/// one back however.
switch ($iReturn) {
case ScramAuth::RESULT_SUCCESS:
// Authentication has succeeded.
$_SESSION['username'] = $sUsername;
break;
case ScramAuth::RESULT_USERNAME:
case ScramAuth::RESULT_PASSWORD:
// Either the username or password was incorrect.
break;
case ScramAuth::RESULT_RESPOND:
// We have a JSON string to return to the user.
break;
default:
// An unknown error has happened. This shouldn't ever be called, but just.
/// in case.
header('HTTP/1.0 500 Internal Server Error');
echo '<h1>An unknown error has occurred.</h1>';
exit(1);
break;
}
// Return the JSON response generated by ScramAuth
header('Content-Type: application/json; charset=utf8');
echo $sReturn;
endif;
Scram = {
// summary: An implementation of th SCRAM-SHA256-JSON authentication protocol
// for use with the Storm Content Management Framework.
// author: Justin "4sak3n 0ne" Jahn
// copyright: (c) 2010 4sak3n Design (http://www.4sk.us/)
// license: http://www.opensource.org/licenses/bsd-license.php New BSD License
// The cryptographic libraries to be utilized by this method.
crypto: {
base: 'http://crypto-js.googlecode.com/files/',
hmac: '2.3.0-crypto-sha256-hmac.js',
pbkdf2: '2.3.0-crypto-sha1-hmac-pbkdf2.js'
},
// A list of currently active error elements
errors: {},
// The request variables
request: [],
bindToForm: function (/*HTMLFormElement*/ element)
{
// summary:
// Bind to a form's onSubmit event to override the default
// functionality.
// element:
// The form element to bind.
// Bind the form
dojo.connect(element, 'onsubmit', Scram.authenticate);
},
authenticate: function(/*Dojo.event*/ event)
{
// summary:
// Attempt the SCRAM authentication with the server.
// event:
// The event object passed in when fired.
// Stop the event from bubbling to prevent the form from loading.
dojo.stopEvent(event);
// The request information. This stores all of the information that is
/// sent and received in an indexed array, ordered by request.
Scram.request[0] = {
n: dojo.attr(event.target.username, 'value'),
r: Crypto.util.randomBytes(16).join('')
};
// Attempt to determine where to submit the JSON to. If the form has an
/// action attribute, then it will be utilized. Otherwise, the window's
/// location will be used.
var sUrl = '';
try {
sUrl = dojo.attr(event.target, 'action');
} catch (e) {
sUrl = window.location;
}
// Fetch the form's submit button and get the label
var oButton = Scram.getSubmitButton(event.target);
var sButton = dojo.attr(oButton, 'value');
// Disable the submit button and change it's label
dojo.attr(oButton, 'disabled', 'disabled');
dojo.attr(oButton, 'value', 'Logging In...');
// Do the initial request. When a valid return value is provided, then
/// phaseTwo will be called to finish up the authentication request.
dojo.xhrPost({
url: sUrl,
postData: dojo.toJson(Scram.request[0]),
handleAs: 'json',
headers: {'Content-Type': 'application/json; charset=utf-8'},
failOk: true,
handle: function(data, IOArgs) {
switch(IOArgs.xhr.status) {
case 500:
Scram.error('An Internal Server Error has occurred.',
event.target);
break;
case 404:
Scram.error('Unable to contact the authentication server.',
event.target);
break;
case 200:
// Check to see if the request was an error
if (data.result) {
Scram.error(data.message, event.target);
} else {
// Set the server-provided variables as a variable for later
/// use.
Scram.request[1] = data;
// Call the phaseTwo method to handle the remainder of the
/// request.
Scram.phaseTwo(event.target);
}
break;
default:
// Default to letting them know that something odd has
/// happened.
Scram.error('An unknown error has occurred.',
event.target);
break;
}
// Restore the submit button's label and turn it back on
dojo.removeAttr(oButton, 'disabled');
dojo.attr(oButton, 'value', sButton);
}
});
},
phaseTwo: function(/*HTMLFormElement*/ form)
{
// summary:
// Create the information necessary to complete a SCRAM authentication
// request, and check for a server success message.
// form:
// The form element.
// Shorten the request variable
var oRequest = Scram.request;
// Form the passwords. In our implementation the password is hashed before
/// the
var sPassword = Crypto.SHA256(dojo.attr(form.password, 'value'));
// Use the PBKDF2 method to generate the proper password
var sSaltedPassword = Crypto.PBKDF2(sPassword, oRequest[1].s, 64, {
hasher: Crypto.SHA256,
iterations: oRequest[1].i
});
// Generate the ClientKey and StoredKey
var sClientKey = Crypto.SHA256(sSaltedPassword);
var sStoredKey = Crypto.SHA256(sClientKey);
// Generate the AuthMessage
var sAuthMessage = 'n=' + dojo.attr(form.username, 'value') + ',r=' + oRequest[0].r
+ ',r=' + oRequest[1].r + ',s=' + oRequest[1].s + ',i=' + oRequest[1].i
+ ',r=' + oRequest[1].r;
// Generate the ClientSignature
var sClientSignature = Crypto.HMAC(Crypto.SHA256, sStoredKey, sAuthMessage);
// Generate the ClientProof
var sClientProof = Scram._xor(sClientKey, sClientSignature);
// Convert the client proof into an array of bytes and then into base64
/// for transportation to the backend.
sClientProof = Crypto.charenc.Binary.stringToBytes(sClientProof);
sClientProof = Crypto.util.bytesToBase64(sClientProof);
// Form the final request. This contains a clientnonceservernonce and the
/// ClientProof, which is used in conjunction with the server's known info
/// to authenticate the client.
oRequest[2] = {
r: oRequest[1].r,
p: sClientProof
};
// If the form has an action property set, then use it. Otherwise, fall
/// back to the current page.
var sUrl = '';
try {
sUrl = dojo.attr(form, 'action');
} catch (e) {
sUrl = window.location;
}
// Do the final request
dojo.xhrPost({
url: sUrl,
postData: dojo.toJson(oRequest[2]),
handleAs: 'json',
headers: {'Content-Type': 'application/json; charset=utf-8'},
failOk: true,
handle: function(data, IOArgs) {
switch(IOArgs.xhr.status) {
case 500:
Scram.error('An Internal Server Error has occurred.',
form);
break;
case 404:
Scram.error('Unable to contact the authentication server.',
form);
break;
case 200:
// Check to see if there is a message
if (data.result) {
// Tell them the message
Scram.error(data.message, form, data.result);
// Redirect if it's a success message
if (data.result == 'success' && form.referrer) {
// There was, let's redirect as needed.
window.location = dojo.attr(form.referrer, 'value');
}
} else {
// An unknown error has occurred
Scram.error('An unknown error has occurred.', form)
}
break;
default:
// Default to letting them know that something odd has
/// happened.
Scram.error('An unknown error has occurred.', form);
break;
}
}
});
},
getSubmitButton: function(/*HTMLFormElement*/ form)
{
// summary:
// Fetch the submit button from the provided form.
// form:
// The form element whose button to fetch.
// return:
// HTMLSubmitElement The submit button.
// Get the form's ID.
var sId = dojo.attr(form, 'id');
// Form the query
var sQuery = '#' + sId + ' input[type=submit]';
// Do the query
var aObjects = dojo.query(sQuery);
// Return the first button
return aObjects[0];
},
error: function(/*String*/ message, /*HTMLFormElement*/ form, /*String?*/ type)
{
// summary:
// Display an error message for the user.
// message:
// The message to be displayed.
// form:
// The form the message should associate itself with.
// type:
// The class to utilize as a message type. Defaults to 'error'.
// Check the type, and make sure it's a string
if (typeof type != 'string') { type = 'error'; }
// Create an error element
var sId = dojo.attr(form, 'id');
// If we don't already have an element to send the message with, let's
/// create one.
if (!Scram.errors.sId) {
// Create the element
var element = dojo.create('p', {innerHTML: message}, form, 'before');
// Set the element for use later
Scram.errors.sId = element;
}
// If an element already exists, fade it out and fade the new one in
dojo.attr(Scram.errors.sId, 'innerHTML', message);
dojo.attr(Scram.errors.sId, 'class', type);
},
_xor: function (/*String*/txt1, /*String*/txt2)
{
// summary:
// Perform an XOR on the two given strings.
// text:
// The first string.
// key:
// The second string
// return:
// String The two provided strings XORed together.
// The output buffer
var sBuffer = '';
// An array of characters to character codes
var ord = [];
// Loop through each character code and create a mapping
for (z = 1; z <= 255; z++) {
ord[String.fromCharCode(z)] = z;
}
// Loop through each character in the first string
for (j = z = 0; z < txt1.length; z++) {
// Add the character code generated from the XOR of the two character codes.
sBuffer += String.fromCharCode(ord[txt1.substr(z, 1)] ^ ord[txt2.substr(j, 1)]);
// Make sure that the second string is long enough, and if not, reset
/// that index to zero.
j = (j < txt2.length) ? j + 1 : 0;
}
return sBuffer;
}
};
// This library is dependant on the Crypto-JS libraries, which have been custom
/// compiled for this scenario.
dojo.addOnLoad(function() {
var oCrypto = Scram.crypto;
var sBase = oCrypto.base;
dojo.create('script', {
type: 'text/javascript',
src: oCrypto.base + oCrypto.pbkdf2
}, dojo.body());
dojo.create('script', {
type: 'text/javascript',
src: oCrypto.base + oCrypto.hmac
}, dojo.body());
// Bind the Scram object to the form
Scram.bindToForm(dojo.byId('login'));
});
<?php
/**
* @author Justin "4sak3n 0ne" Jahn
* @copyright (c) 2011 4sak3n Design (http://www.4sk.us/)
* @license http://www.opensource.org/licenses/bsd-license.php New BSD License
*/
/**
* Passwords are dealt with as objects for ease of use.
*/
require_once('ScramPassword.php');
/**
* Authentication mechanism for the SCRAM-SHA256-JSON protocol.
*
* @package Scram
* @author Justin "4sak3n 0ne" Jahn
* @copyright (c) 2011 4sak3n Design (http://www.4sk.us/)
* @license http://www.opensource.org/licenses/bsd-license.php New BSD License
*/
class ScramAuth
{
/**
* The hashing algorithm to utilize.
*
* @var string
*/
const ALGORITHM = 'sha256';
/**#@+
* Possible results of the {@link ScramAuth::authenticate()} method.
*
* @var int
*/
const RESULT_RESPOND = 0;
const RESULT_SUCCESS = 1;
const RESULT_USERNAME = 2;
const RESULT_PASSWORD = 3;
/**#@-*/
/**#@+
* Possible error codes thrown by {@link ScramAuth::authentiate()} method.
*
* @var int
*/
const ERROR_INVALID = 10;
const ERROR_USERNAME = 11;
const ERROR_PASSWORD = 12;
const ERROR_JSON = 13;
const ERROR_PHASE = 14;
const ERROR_NONCE = 15;
/**#@-*/
/**
* The identity of the user.
*
* @var string
*/
protected $_username;
/**
* The {@link ScramPassword} object for the user.
*
* @var ScramPassword
*/
protected $_password;
/**
* A {@link StdClass} used to store session data.
*
* @var StdClass
*/
protected $_session;
/**
* A response string to be returned to the client.
*
* @var string
*/
protected $_response = null;
/**
* Prepare for authentication.
*
* @param string $username
* @param ScramPassword $password
* @return void
*/
public function __construct($username, ScramPassword $password)
{
// Set the username and password
$this->_username = $username;
$this->_password = $password;
// Start the session if it's not already been done
if (session_id() == "") session_start();
// Handle the session
if (!isset($_SESSION[__CLASS__])) $this->_clearSession();
else $this->_session = $_SESSION[__CLASS__];
}
/**
* Process SCRAM input and determine if the user is authorized.
*
* @throws InvalidArgumentException If the request was invalid.
* @throws Exception If there was an error processing the JSON.
* @return int See RESULT_* constants.
*/
public function authenticate()
{
// Get the raw body
$sRawBody = trim(file_get_contents('php://input'));
// Try to turn the request into a JSON array
try {
$aRequest = json_decode($sRawBody);
$aRequest = new ArrayObject($aRequest);
} catch (Exception $e) {
// Set the code to something we can deal with later
$e->setCode(self::ERROR_JSON);
// Generate the error. This generate a response string for the
/// parent to echo out.
$this->_generateError($e);
// Tell the calling code that a response is ready
return self::RESULT_RESPOND;
}
// If no username and clientnonce has been provided, then we must be in
/// phase one. If we aren't, then this will throw an exception that should
/// be caught by the caller for error handling.
if (!isset($this->_session->n) || !isset($this->_session->r)) {
try {
$aResponse = $this->_phaseOne($aRequest);
$this->_setResponse($aResponse);
} catch (Exception $e) {
// This is a SCRAM-JSON error, and it must be returned to the client
/// as valid JSON.
$this->_generateError($e);
// If the exception happens to be that the user wasn't found, let's
/// return now instead of encoding an error as this is something
/// the parent may want to handle.
if ($e->getCode() === self::ERROR_USERNAME) {
return self::RESULT_USERNAME;
}
}
// Phase one will always need to respond unless the user wasn't found.
return self::RESULT_RESPOND;
}
// The client has moved on to phase two. If the request checks out,
/// it's time to move to the authentication phase. Otherwise we start
/// from the beginning.
if (isset($aRequest['r']) && isset($aRequest['p'])) {
try {
$iReturn = $this->_phaseTwo($aRequest);
} catch (Exception $e) {
// Generate the error in JSON format
$this->_generateError($e);
return self::RESULT_RESPOND;
}
// The return value varies rapidly in phase two depending on the
/// problem (or success) encountered.
return $iReturn;
}
// We have an invalid JSON request, sent it back to the caller as such
/// so that they may handle it.
$oException = new InvalidArgumentException('Invalid SCRAM request.',
self::ERROR_INVALID);
// Generate an error based on the exception we just made.
$this->_generateError($oException);
return self::RESULT_RESPOND;
}
/**
* Fetch the JSON response string generated by the {@link authenticate()}
* method.
*
* @return string
*/
public function getResponse()
{
return $this->_response;
}
/**
* Generate a SCRAM-JSON error based on the provided exception.
*
* @return void
*/
protected function _generateError(Exception $e)
{
// Generate an error array
$aReturn = array(
'result' => 'error',
'message' => $e->getMessage(),
'code' => $e->getCode()
);
// Call the setResonse() method to handle the rest
$this->_setResponse($aReturn);
// We are done with the session at this point. SCRAM requires a new
/// authentication process to begin for failed attempts.
$this->_clearSession();
}
/**
* Generate a response and save it for later.
*
* @param array $response A valid SCRAM-JSON response.
* @return void
*/
protected function _setResponse(array $response)
{
// Try to encode the response
try {
$sResponse = json_encode($response);
} catch (Exception $e) {
$sResponse = array('error' => 'Unknown error.', 'code' => self::ERROR_INVALID);
$sResponse = json_encode($sResponse);
}
// Set the response to a class variable for later use
$this->_response = $sResponse;
}
/**
* Clear the session variable.
*
* @return void
*/
protected function _clearSession()
{
// Create a new session object and set it
$_SESSION[__CLASS__] = new StdClass();
$this->_session = $_SESSION[__CLASS__];
}
/**
* Generates the authentication message from the session data and returns it
* as a string.
*
* @return string
*/
private function _generateAuthMessage()
{
// The AuthMessage is pretty much a concatenation of values during
/// the communication process:
///
/// client-first-message + "," +
/// server-first-message + "," +
/// client-final-message-without-proof
$sAuth = sprintf('n=%1$s,r=%2$s,r=%3$s,s=%4$s,i=%5$d,r=%3$s',
$this->_session->n,
$this->_session->r['c'],
implode('', array_values($this->_session->r)),
$this->_session->s,
$this->_session->i);
// Return the AuthMessage to the caller
return $sAuth;
}
/**
* Process Phase ONE of a SCRAM request.
*
* @param array $request An array of request variables from the SCRAM operation.
* @return array The response array.
*/
private function _phaseOne($request)
{
// We are in phase one. Make sure that the client has provided us
/// with a username and a clientnonce.
if (!isset($request['n']) || !isset($request['r'])) {
// Throw the exception
throw new Exception('Server phase one expects a username and clientnonce.',
self::ERROR_INVALID);
}
// Check the username
if ($request['n'] !== $this->_username) {
throw new InvalidArgumentException('User not found.', self::ERROR_USERNAME);
}
// Reset the session, it shouldn't even have anything in it anyways
$this->_clearSession();
// We have a valid user associated with this request, store the information
/// and form the return information. For an explaination of what these
/// one-letter keys mean, please refer to the SCRAM specification.
$sNonce = ScramPassword::generateRandomHash();
$this->_session->n = $request['n'];
$this->_session->r = array();
$this->_session->r['c'] = $request['r'];
$this->_session->r['s'] = $sNonce;
$this->_session->i = $this->_password->getIterations();
$this->_session->s = $this->_password->getSalt();
// Generate the response object
$aResponse = array(
'r' => $this->_session->r['c'] . $this->_session->r['s'],
's' => $this->_session->s,
'i' => $this->_session->i
);
// Return the generated response
return $aResponse;
}
/**
* Perform the steps necessary to complete phase two of the SCRAM authentication
* process.
*
* Phase two involves the client sending us a ClientProof so that we may compare
* it against the user's credentials in the database. It does this by computing
* the ClientSignature from the previously sent messages and a triple-hashed
* version of the user's password. It then XORs it with the ClientProof that was
* sent, which rebuilds the triple-hashed password. It is then compared with
* the password found in the database using the same hashing methods.
*
* @throws Exception If the client's nonce was invalid or phasing is mixed up.
* @param array $request An array of client request information.
* @return int See class constants.
*/
private function _phaseTwo($request)
{
// Sanity check
if (!isset($this->_session->r)) {
throw new Exception(
'Client requested phase two without calling phase one.',
self::ERROR_PHASE
);
}
// Verify the nonce. By specifications an invalid nonce means that the
/// request for authentication must be terminated.
$sNonce = implode('', array_values($this->_session->r));
if ($sNonce != $request['r']) {
throw new Exception(
'Invalid nonce sent by client during phase two.',
self::ERROR_NONCE
);
}
// Generate the AuthMessage and ClientSignature
$sAuthMessage = $this->_generateAuthMessage();
$sClientKey = hash(self::ALGORITHM, $this->_password->getPassword());
$sStoredKey = hash(self::ALGORITHM, $sClientKey);
$sClientSignature = hash_hmac(self::ALGORITHM, $sStoredKey, $sAuthMessage);
$sClientProof = base64_decode($request['p']);
// Fetch the ClientKey from the ClientSignature and the ClientProof
$sNewClientKey = ScramPassword::xorString($sClientSignature, $sClientProof);
// Hash again, and compare to the stored key as per specifications
$sNewClientKey = hash(self::ALGORITHM, $sNewClientKey);
// If it matches, then we have an authenticated user
if ($sNewClientKey == $sStoredKey) {
// Generate a response
$this->_setResponse(array(
'result' => 'success',
'username' => $this->_session->n,
'message' => 'The authentication was successful.'
));
// We are done with the session at this point
$this->_clearSession();
// Notify the parent code that it was a successful authentication
return self::RESULT_SUCCESS;
} else {
// Create an exception object
$oException = new InvalidArgumentException(
'The provided credential was invalid.',
self::ERROR_PASSWORD
);
// Generate a JSON error based on it
$this->_generateError($oException);
// Return that it was a failed password
return self::RESULT_PASSWORD;
}
}
}
<?php
/**
* @author Justin "4sak3n 0ne" Jahn
* @copyright (c) 2011 4sak3n Design (http://www.4sk.us/)
* @license http://www.opensource.org/licenses/bsd-license.php New BSD License
*/
/**
* Password management and cryptographic utilities to help manage a password.
*
* @package Scram
* @author Justin "4sak3n 0ne" Jahn
* @copyright (c) 2011 4sak3n Design (http://www.4sk.us/)
* @license http://www.opensource.org/licenses/bsd-license.php New BSD License
*/
class ScramPassword
{
/**
* The regular expression utilize to find different pieces of the password.
*
* Matches:
* 0: While string
* 1: Hashing algorithm
* 2: PKCS2 Iterations
* 3: Salted Password
*
* @var string
*/
const REGEX = '/^{([a-z0-9]+)}(\d+):([a-z0-9]+)$/i';
/**
* The password itself, which follows the format:
*
* {{algo}}{len}:{salt}{pass}
* - {algo} The hashing algorithm employed in curly brackets.
* - {len} Is a randomly generated pbkdf2 iteration count.
* - {salt} Is a randomly generated SHA256 hash by default.
* - {pass} Is a password using the length of the hash and salt provided.
*
* @var string
*/
protected $_password = null;
/**
* An array of metadata about the current password. This is used as an
* in-memory caching mechanism to help prevent multiple calls to
* {@link preg_match} over the course of this object's lifetime.
*
* Data:
* <code>
* string algo Hashing algorithm used on the current password.,
* int len Number of PKCS2 iterations on the current password.,
* string salt Salt used on the current password.,
* string pass Salted password
* int hashLen The digest string length of the currently used algorithm.
* </code>
*
* @var array
*/
protected $_metadata = array();
/**
* An array of options for this class.
*
* Options:
* <code>
* string salt A string to utilize as salt. Defaults to a random hash.
* int iterations PBKDF2 Iterations. Defaults to mt_rand(100, 300).
* string algorithm A valid algorithm string for use with hash(). Default sha256.
* </code>
*/
protected $_options = array(
'salt' => null,
'iterations' => null,
'algorithm' => null
);
/**
* Set up class, and verify that the input password is in the correct format.
*
* Input passwords may either be in the database format this object generates,
* or a raw input password.
*
* @param string $password A raw password, or raw database string.
* @param array $options An array of options. See {@link $_options}.
* @return void
*/
public function __construct($password, array $options = array())
{
// Sanitization
$password = trim($password);
// Handle options
$options = array_merge($this->_options, $options);
// Call the setters
$this->setAlgorithm($options['algorithm']);
$this->setIterations($options['iterations']);
$this->setSalt($options['salt']);
$this->setPassword($password);
}
/**
* Set the algorithm to be utilized by the password generations methods.
*
* NOTE: The default algorithm is sha256.
* NOTE: If this value is changed, and passwords incoming from the database
* do not have the same hashing function, there is no way to tell, and
* they will be marked for processing, which will break authentication.
*
* @throws \InvalidArgumentException If the algorithm string provided is not valid.
* @param string $algorithm A valid algorithm for use with {@link hash()}.
* @return void
*/
public function setAlgorithm($algorithm = null)
{
// If the algorithm isn't set, then default it
if ($algorithm === null) $algorithm = 'sha256';
// Sanity check
if (!in_array($algorithm, hash_algos())) {
throw new \InvalidArgumentException('Provided hash algorithm invalid: '
. $algorithm);
}
// It's valid, set it for later use.
$this->_options['algorithm'] = $algorithm;
}
/**
* Returns the algorithm string used with {@link hash()} to provide a
* password.
*
* @return string
*/
public function getAlgorithm()
{
if (isset($this->_metadata['algo'])) return $this->_metadata['algo'];
return $this->_options['algorithm'];
}
/**
* Return the string length of a digest generated with the {@link getAlgorithm()}
* as a hashing function.
*
* @return int
*/
public function getAlgorithmLength()
{
if (isset($this->_metadata['hashLen'])) return $this->_metadata['hashLen'];
return strlen(hash($this->getAlgorithm(), null, false));
}
/**
* Set the number of pbkdf2 iterations to run.
*
* NOTE: This value is only used with incoming new passwords.
*
* @param int $iterations The number of iterations. Defaults to a random number.
* @return void
*/
public function setIterations($iterations = null)
{
// If we have a null value, let's generate one.
if ($iterations === null) $iterations = mt_rand(100, 300);
// Sanity check
if (intval($iterations < 1)) {
throw new \Exception('Invalid iteration count provided.');
}
// Set the option
$this->_options['iterations'] = $iterations;
}
/**
* Get the number of pbkdf2 iterations needed to generate the password.
*
* NOTE: The return value may not be the same as the one provided in
* {@link setIterations()}, depending on if the incoming password is
* new or from the database.
*
* @return int
*/
public function getIterations()
{
if (isset($this->_metadata['len'])) return $this->_metadata['len'];
return $this->_options['iterations'];
}
/**
* Set the salt used to generate a password.
*
* NOTE: The salt is only utilized when generating NEW password hashes.
*
* @param string $salt The salt to utilize. Defaults to a random hash.
* @return void
*/
public function setSalt($salt = null)
{
// If we were given a null value for salt, fix it
if ($salt === null) $salt = self::generateRandomHash();
// Hash the salt to ensure consistency
$salt = hash($this->getAlgorithm(), $salt);
// Set it
$this->_options['salt'] = $salt;
}
/**
* Fetch the password salt. The password salt is a randomly generated SHA256
* hash. It is different for each user, and will change when the user's password
* is updated.
*
* NOTE: The salt may or may not be the value provided in {@link setSalt()},
* depending on if the password provided is from the database or new.
*
* @return string A digest generated utilizing the algorithm provided in options.
*/
public function getSalt()
{
if (isset($this->_metadata['salt'])) return $this->_metadata['salt'];
return $this->_options['salt'];
}
/**
* Set the password, generating a new one if it did not come from the db.
*
* @param string $password A password string.
* @return void
*/
public function setPassword($password)
{
// If the password isn't from the database already, salt it and format it.
if (!$this->_isSalted($password)) $password = $this->_saltPassword($password);
// Generate metadata for the password
$this->_metadata = $this->_generateMetadata($password);
// Set the password
$this->_password = $password;
}
/**
* Fetch the pbkdf2 digest from the database.
*
* @return string
*/
public function getPassword()
{
if (isset($this->_metadata['pass'])) return $this->_metadata['pass'];
return null;
}
/**
* Generate metadata from the provided password hash and return it for use.
*
* @param string $password A raw password.
* @return array
*/
protected function _generateMetadata($password)
{
// Run the regular expression match on the provided password
$aMatch = array();
$iMatch = preg_match(self::REGEX, $password, $aMatch);
// Form the metadata array
$iLength = strlen(hash($aMatch[1], null, false));
$sSalt = substr($aMatch[3], 0, $iLength);
$sPassword = substr($aMatch[3], $iLength);
$aMetadata = array(
'algo' => $aMatch[1],
'len' => $aMatch[2],
'salt' => $sSalt,
'pass' => $sPassword,
'hashLen' => $iLength
);
// Return the newly generated array
return $aMetadata;
}
/**
* Determine if the provided password is salted.
*
* @throws InvalidArgumentException If the provided password isn't salted/hash.
* @param string $password The password hash to verify.
* @return bool
*/
protected function _isSalted($password)
{
// If the match is successful, then the password is from the database
if (preg_match(self::REGEX, $password) > 0) return true;
return false;
}
/**
* Salt the provided hash.
*
* @return string
*/
protected function _saltPassword($password)
{
// Create some salt, and use it to generate the password hash
$sSalt = $this->getSalt();
$iIterations = $this->getIterations();
$sAlgorithm = $this->getAlgorithm();
$iKeylen = $this->getAlgorithmLength();
// Create the password hash
$sPassword = self::pbkdf2($password,
$sSalt,
$iIterations,
$iKeylen,
$sAlgorithm);
// Generate a raw password based on our custom format
$sPassword = sprintf('{%s}%d:%s',
$sAlgorithm,
$iIterations,
$sSalt . $sPassword);
// Return the password
return $sPassword;
}
/**
* Generate a random hash based on the currently installed module.
*
* @return string A 64 character hash.
*/
public static function generateRandomHash()
{
// If the mcrypt libraries are installed, generate a hash using it's
/// methods.
if (function_exists('mcrypt_create_iv')) {
$sHash = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
} else {
// We don't have the mcrypt library, so generate a random number
/// and a hash based on timestamp.
$sHash = hash('sha256', mt_rand() . uniqid());
}
// Return the generated hash
return $sHash;
}
/**
* Implementation of the PBKDF2 key derivation function as described in RFC 2898.
*
* PBKDF2 was published as part of PKCS #5 v2.0 by RSA Security. The standard is
* also documented in IETF RFC 2898.
*
* @param string $password An SHA256 password hash.
* @param string $salt The salt to utilize when hashing.
* @param int $iter_count The number of times to iterate.
* @param int $keylen The number of characters to return.
* @param string $hash_alg A hash algorithm to utilize
* @return string PBKDF2 digest of the specified length.
*
* http://www.rsa.com/rsalabs/node.asp?id=2127
* http://www.rfc-editor.org/rfc/rfc3962.txt
*/
public static function pbkdf2($password, $salt, $iter_count=4096,
$keylen=64, $hash_alg = 'sha256')
{
// Compute the length of hash alg output.
// Some folks use a static variable and save the value of the hash len.
// Considering we are doing 1000s hmacs, doing one more won't hurt.
$hashlen = strlen(hash($hash_alg, null, true));
// compute number of blocks need to make $keylen number of bytes
$numblocks = ceil($keylen / $hashlen);
// blocks are appended to this
$output = '';
for ($i = 1; $i <= $numblocks; ++$i) {
$block = hash_hmac($hash_alg, $salt . pack('N', $i), $password, true);
$ib = $block;
for ($j = 1; $j < $iter_count; ++$j) {
$block = hash_hmac($hash_alg, $block, $password, true);
$ib ^= $block;
}
$output .= $ib;
}
// extract the right number of output bytes
$sReturn = substr($output, 0, $keylen);
// Convert the binary characters to hexidecimal
$sReturn = bin2hex($sReturn);
// Return the result
return $sReturn;
}
/**
* A mult-byte implementation of XOR.
*
* @see http://stackoverflow.com/questions/7548667/xor-encryption-in-php
* @param string $text The input text.
* @param string $key The input to xor with.
* @return string The XOR of the two values.
*/
public static function xorString($text, $key)
{
// The return value and a counter for use when xoring
$i = 0;
$iKey = strlen($key);
$sReturn = '';
// Loop through each character, xoring sequentially based on the key
foreach(str_split($text) as $sCharacter) {
// Get the character to use from our key
$iCharacter = $i++ % $iKey;
$sKey = ord($key[$iCharacter]);
$sCharacter = ord($sCharacter);
// Perform the XOR operation and get an ASCII character
$sReturn .= chr($sCharacter ^ $sKey);
}
// Return the result
return $sReturn;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment