Skip to content

Commit

Permalink
feature(security): Adds functions to create and validate HMAC tokens
Browse files Browse the repository at this point in the history
Also adds simple tests for HMAC.

Fixes Elgg#7824
  • Loading branch information
mrclay committed Mar 11, 2015
1 parent 4df428f commit 592a508
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 6 deletions.
27 changes: 27 additions & 0 deletions docs/guides/actions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -425,3 +425,30 @@ You can also access the tokens from javascript:
These are refreshed periodically so should always be up-to-date.


Security Tokens
===============
If you need to pass data to a user and trust that the user has returned it to you unaltered, you should use
``elgg_generate_mac()`` to generate an authentication code to pass alongside the data. You can then validate
this code using ``elgg_validate_mac()``.

.. code:: php
// generate a querystring such that $a and $b can't be altered
$a = 1234;
$b = "hello";
$query = http_build_query([
'a' => $a,
'b' => $b,
'mac' => elgg_generate_mac([$a, $b]),
]);
$url = "action/foo?$query";
// validate the querystring
$a = get_input('a', '', false);
$b = get_input('b', '', false);
$mac = get_input('mac', '', false);
if (elgg_validate_mac($mac, [$a, $b])) {
// $a and $b have not been altered
}
4 changes: 2 additions & 2 deletions engine/classes/Elgg/ActionsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,10 @@ public function generateActionToken($timestamp) {
$site_secret = _elgg_services()->siteSecret->get();
$session_id = _elgg_services()->session->getId();
// Session token
$st = _elgg_services()->session->get('__elgg_session');
$session_token = _elgg_services()->session->get('__elgg_session');

if ($session_id && $site_secret) {
return _elgg_services()->crypto->getHmac($timestamp . $session_id . $st, $site_secret, 'md5');
return _elgg_services()->crypto->getHmac([$timestamp, $session_id, $session_token], '', 'md5');
}

return false;
Expand Down
13 changes: 9 additions & 4 deletions engine/classes/ElggCrypto.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,20 @@ public function getRandomBytes($length) {
/**
* Generate a MAC with output in Base64URL encoding
*
* @param string $data Data we're creating a MAC for
* @param string $key HMAC key. uses elgg site secret if none given
* @param string $algo HMAC hash algorithm
* As a MAC often needs to validate several values, you can pass an array of strings as $data.
*
* @param string|string[] $data Data we're creating a MAC for
* @param string $key HMAC key. uses elgg site secret if none given
* @param string $algo HMAC hash algorithm
* @return string
*/
function getHmac($data, $key = '', $algo = 'sha256') {
public function getHmac($data, $key = '', $algo = 'sha256') {
if (!$key) {
$key = _elgg_services()->siteSecret->get();
}
if (is_array($data)) {
$data = serialize(array_map('strval', $data));
}
$bytes = hash_hmac($algo, $data, $key, true);
return strtr(rtrim(base64_encode($bytes), '='), '+/', '-_');
}
Expand Down
32 changes: 32 additions & 0 deletions engine/lib/actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,38 @@ function elgg_unregister_action($action) {
return _elgg_services()->actions->unregister($action);
}

/**
* Generate an authentication code/token in Base64URL encoding, using the site secret as key.
*
* As a MAC often needs to validate several values, you can pass an array of strings as $data.
*
* @param string[]|string $data Data passed through an untrusted channel
* @param string $algo Hash algorithm used in HMAC
*
* @return string
* @since 1.11
* @see elgg_validate_mac
*/
function elgg_generate_mac($data, $algo = 'sha256') {
return _elgg_services()->crypto->getHmac($data, '', $algo);
}

/**
* Validate an authentication code generated via elgg_generate_mac().
*
* @param string $mac MAC given in input, in Base64URL format
* @param string[]|string $data Data ostensibly unaltered by the user
* @param string $algo Hash algorithm used in HMAC
*
* @return bool
* @since 1.10
* @see elgg_generate_mac
*/
function elgg_validate_mac($mac, $data, $algo = 'sha256') {
$crypto = _elgg_services()->crypto;
return $crypto->areEqual($mac, $crypto->getHmac($data, '', $algo));
}

/**
* Validate an action token.
*
Expand Down
31 changes: 31 additions & 0 deletions engine/tests/phpunit/ElggCryptoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ class ElggCryptoTest extends \PHPUnit_Framework_TestCase {
*/
protected $stub;

/**
* @see ElggCrypto
* @see ElggCrypto::getRandomBytes
*/
protected function setUp() {
$this->stub = $this->getMockBuilder('\ElggCrypto')
->setMethods(array('getRandomBytes'))
Expand Down Expand Up @@ -41,4 +45,31 @@ function provider() {
function testGetRandomString($length, $chars, $expected) {
$this->assertSame($expected, $this->stub->getRandomString($length, $chars));
}

function testCanGenerateMacs() {
$crypto = new ElggCrypto();
$key = 'a very bad key';

$mac = $crypto->getHmac(1234, $key);
$this->assertTrue((bool)preg_match('~^[a-zA-Z0-9\-_]{30,}$~', $mac));

$mac = $crypto->getHmac([1234, 'foo'], $key);
$this->assertTrue((bool)preg_match('~^[a-zA-Z0-9\-_]{30,}$~', $mac));
}

function testStringCastDoesntAffectMacs() {
$crypto = new ElggCrypto();
$key = 'a very bad key';

$this->assertEquals($crypto->getHmac(1234, $key), $crypto->getHmac('1234', $key));
$this->assertEquals($crypto->getHmac([1234, 'hello'], $key), $crypto->getHmac(['1234', 'hello'], $key));
}

function testStringVariationsAlterMac() {
$crypto = new ElggCrypto();
$key = 'a very bad key';

$this->assertNotEquals($crypto->getHmac(1234, $key), $crypto->getHmac(1235, $key));
$this->assertNotEquals($crypto->getHmac([1234, 'hello'], $key), $crypto->getHmac(['1234', 'hellO'], $key));
}
}

0 comments on commit 592a508

Please sign in to comment.