Created
October 19, 2011 19:43
-
-
Save justincjahn/1299443 to your computer and use it in GitHub Desktop.
SCRAM-SHA256-JSON
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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