Skip to content

Commit b8afafe

Browse files
committed
Support for scrypt hashing
1 parent 455904f commit b8afafe

File tree

2 files changed

+191
-2
lines changed

2 files changed

+191
-2
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,27 @@ Download the files and require them in your project.
1616

1717
`require '/secure-functions/src/SecureFuncs.php';`
1818

19+
## Scrypt and Encryption
20+
21+
#### [domBlack/php-scrypt](https://github.com/DomBlack/php-scrypt)
22+
Install the php module if you want to use scrypt hashing
23+
24+
25+
#### [defuse/php-encryption](https://github.com/defuse/php-encryption)
26+
Is required if you want to make use of his encryption library. Will be automatically installed with composer
27+
28+
29+
1930
## Usage
2031
All functions are static public functions right now so you can simply call the functions like this:
2132

2233
`SecureFuncs\SecureFuncs::password_hash('input');`
2334

2435
## Functions
2536

37+
### compareStrings($string1, $string2)
38+
Compare strings while preventing timed attacks
39+
2640
### decrypt($input, $key)
2741
Returns the decryped output as a string using [defuse/php-encryption](https://github.com/defuse/php-encryption)'s library.
2842

@@ -50,6 +64,12 @@ Return a random key using [defuse/php-encryption](https://github.com/defuse/php-
5064
### randomString($length)
5165
Returns a random string for the given length
5266

67+
### scryptcheck($password, $hash)
68+
Compare a password and hash using [DomBlack/php-scrypt](https://github.com/DomBlack/php-scrypt)
69+
70+
### scrypthash($password, $salt, $cpu, $memory, $parallel)
71+
Hash a password using DomBlack's php scrypt library
72+
5373
### setFormToken($id)
5474
Set a unique token in the session and returns it, can be used to verify post/get requests
5575

src/SecureFuncs.php

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
class SecureFuncs
55
{
66

7-
public static $secret;
7+
/**
8+
* @var int The key length
9+
*/
10+
private static $_keyLength = 32;
811

912
/**
1013
* @param $input
@@ -67,7 +70,7 @@ public static function getFormToken($id, $token, $limit_time = 300)
6770
// If time limit is set, check if isset
6871
if ($limit_time !== false) {
6972
// if time < limit time return true/false
70-
if (empty($_SESSION['formtoken_time'][$id]) || $_SESSION['formtoken_time'][$id] < time() - $limit_time){
73+
if (empty($_SESSION['formtoken_time'][$id]) || $_SESSION['formtoken_time'][$id] < time() - $limit_time) {
7174
$valid = false;
7275
}
7376
}
@@ -213,4 +216,170 @@ public static function pseudoBytes($length = 1)
213216
}
214217
}
215218

219+
/**
220+
* Scrypt functions
221+
*
222+
* Based on php-scrypt - https://github.com/DomBlack/php-scrypt
223+
*/
224+
225+
/**
226+
* Generates a random salt
227+
*
228+
* @param int $length The length of the salt
229+
*
230+
* @return string The salt
231+
*/
232+
public static function generateSalt($length = 8)
233+
{
234+
$buffer = '';
235+
$buffer_valid = false;
236+
if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
237+
$buffer = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
238+
if ($buffer) {
239+
$buffer_valid = true;
240+
}
241+
}
242+
if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
243+
$cryptoStrong = false;
244+
$buffer = openssl_random_pseudo_bytes($length, $cryptoStrong);
245+
if ($buffer && $cryptoStrong) {
246+
$buffer_valid = true;
247+
}
248+
}
249+
if (!$buffer_valid && is_readable('/dev/urandom')) {
250+
$f = fopen('/dev/urandom', 'r');
251+
$read = static::strlen($buffer);
252+
while ($read < $length) {
253+
$buffer .= fread($f, $length - $read);
254+
$read = static::strlen($buffer);
255+
}
256+
fclose($f);
257+
if ($read >= $length) {
258+
$buffer_valid = true;
259+
}
260+
}
261+
if (!$buffer_valid || static::strlen($buffer) < $length) {
262+
$bl = static::strlen($buffer);
263+
for ($i = 0; $i < $length; $i++) {
264+
if ($i < $bl) {
265+
$buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
266+
} else {
267+
$buffer .= chr(mt_rand(0, 255));
268+
}
269+
}
270+
}
271+
$salt = str_replace(array('+', '$'), array('.', ''), base64_encode($buffer));
272+
273+
return $salt;
274+
}
275+
276+
/**
277+
* Create a password hash
278+
*
279+
* @param string $password The clear text password
280+
* @param string|bool $salt The salt to use, or null to generate a random one
281+
* @param int $N The CPU difficultly (must be a power of 2, > 1)
282+
* @param int $r The memory difficultly
283+
* @param int $p The parallel difficultly
284+
*
285+
* @throws \Exception
286+
*
287+
* @return string The hashed password
288+
*/
289+
public static function scrypthash($password, $salt = false, $N = 16384, $r = 8, $p = 1)
290+
{
291+
// Check if scrypt extension is available
292+
if (!extension_loaded('scrypt')) {
293+
throw new \Exception('Missing scrypt extension');
294+
}
295+
296+
if ($N == 0 || ($N & ($N - 1)) != 0) {
297+
throw new \InvalidArgumentException("N must be > 0 and a power of 2");
298+
}
299+
300+
if ($N > PHP_INT_MAX / 128 / $r) {
301+
throw new \InvalidArgumentException("Parameter N is too large");
302+
}
303+
304+
if ($r > PHP_INT_MAX / 128 / $p) {
305+
throw new \InvalidArgumentException("Parameter r is too large");
306+
}
307+
308+
if ($salt === false) {
309+
$salt = self::generateSalt();
310+
} else {
311+
// Remove dollar signs from the salt, as we use that as a separator.
312+
$salt = str_replace(array('+', '$'), array('.', ''), base64_encode($salt));
313+
}
314+
315+
$hash = scrypt($password, $salt, $N, $r, $p, self::$_keyLength);
316+
317+
return $N . '$' . $r . '$' . $p . '$' . $salt . '$' . $hash;
318+
}
319+
320+
/**
321+
* Check a clear text password against a hash
322+
*
323+
* @param string $password The clear text password
324+
* @param string $hash The hashed password
325+
*
326+
* @throws \Exception
327+
*
328+
* @return boolean If the clear text matches
329+
*/
330+
public static function scryptcheck($password, $hash)
331+
{
332+
// Check if scrypt extension is available
333+
if (!extension_loaded('scrypt')) {
334+
throw new \Exception('Missing scrypt extension');
335+
}
336+
337+
// Is there actually a hash?
338+
if (!$hash) {
339+
return false;
340+
}
341+
342+
list ($N, $r, $p, $salt, $hash) = explode('$', $hash);
343+
344+
// No empty fields?
345+
if (empty($N) or empty($r) or empty($p) or empty($salt) or empty($hash)) {
346+
return false;
347+
}
348+
349+
// Are numeric values numeric?
350+
if (!is_numeric($N) or !is_numeric($r) or !is_numeric($p)) {
351+
return false;
352+
}
353+
354+
$calculated = scrypt($password, $salt, $N, $r, $p, self::$_keyLength);
355+
356+
// Use compareStrings to avoid timeing attacks
357+
return self::compareStrings($hash, $calculated);
358+
}
359+
360+
/**
361+
* Prevent timing attacks
362+
*
363+
* @param string $expected
364+
* @param string $actual
365+
*
366+
* @return boolean If the two strings match.
367+
*/
368+
public static function compareStrings($expected, $actual)
369+
{
370+
$expected = (string)$expected;
371+
$actual = (string)$actual;
372+
$lenExpected = static::strlen($expected);
373+
$lenActual = static::strlen($actual);
374+
$len = min($lenExpected, $lenActual);
375+
376+
$result = 0;
377+
for ($i = 0; $i < $len; $i++) {
378+
$result |= ord($expected[$i]) ^ ord($actual[$i]);
379+
}
380+
$result |= $lenExpected ^ $lenActual;
381+
382+
return ($result === 0);
383+
}
384+
216385
}

0 commit comments

Comments
 (0)