چگونه پسوردهای کاربران را به نحو امنی هش کنیم

هشدار! استفاده از الگوریتم و کدهای موجود در این پست دیگر توصیه نمیشود، چراکه الگوریتم اصولی تری بنام bcrypt وجود دارد که در پست جدیدی آن را معرفی و در اختیار گذاشته ام: http://www.hamidreza-mz2.tk/?p=1091

بنده از این روش در پروژهء خودم استفاده کردم:

<?php

$secure_hash_rounds=16;

$pepper="89JPa36HW7Uiq348dX10ks";

//==================================

$request_entropy=sha1(microtime().$pepper.$_SERVER['REMOTE_ADDR'].
$_SERVER['REMOTE_PORT'].$_SERVER['HTTP_USER_AGENT'].
serialize($_POST).serialize($_GET).serialize($_COOKIE));

//==================================

$entropy=sha1($pepper.$request_entropy);

function crypt_random($min = 0, $max = 0x7FFFFFFF)
{
if ($min == $max) {
return $min;
}

global $entropy;

if (function_exists('openssl_random_pseudo_bytes')) {
// openssl_random_pseudo_bytes() is slow on windows per the following:
// http://stackoverflow.com/questions/1940168/openssl-random-pseudo-bytes-is-slow-php
if ((PHP_OS & "\xDF\xDF\xDF") !== 'WIN') { // PHP_OS & "xDFxDFxDF" == strtoupper(substr(PHP_OS, 0, 3)), but a lot faster
extract(unpack('Nrandom', pack('H*', sha1(openssl_random_pseudo_bytes(4).$entropy.microtime()))));
return abs($random) % ($max - $min) + $min;
}
}

// see http://en.wikipedia.org/wiki//dev/random
static $urandom = true;
if ($urandom === true) {
// Warning's will be output unles the error suppression operator is used. Errors such as
// "open_basedir restriction in effect", "Permission denied", "No such file or directory", etc.
$urandom = @fopen('/dev/urandom', 'rb');
}
if (!is_bool($urandom)) {
extract(unpack('Nrandom', pack('H*', sha1(fread($urandom, 4).$entropy.microtime()))));
// say $min = 0 and $max = 3. if we didn't do abs() then we could have stuff like this:
// -4 % 3 + 0 = -1, even though -1 < $min
return abs($random) % ($max - $min) + $min;
}

if(function_exists('mcrypt_create_iv') and version_compare(PHP_VERSION, '5.3.0', '>=')) {
@$tmp16=mcrypt_create_iv(4, MCRYPT_DEV_URANDOM);
if($tmp16!==false) {
extract(unpack('Nrandom', pack('H*', sha1($tmp16.$entropy.microtime()))));
return abs($random) % ($max - $min) + $min;
}
}

/* Prior to PHP 4.2.0, mt_srand() had to be called before mt_rand() could be called.
Prior to PHP 5.2.6, mt_rand()'s automatic seeding was subpar, as elaborated here:

www.suspekt.org/2008/08/17/mt_srand-and-not-so-random-numbers/

The seeding routine is pretty much ripped from PHP's own internal GENERATE_SEED() macro:

http://svn.php.net/viewvc/php/php-src/tags/php_5_3_2/ext/standard/php_rand.h?view=markup */
static $seeded;
if (!isset($seeded) and version_compare(PHP_VERSION, '5.2.5', '<=')) {
$seeded = true;
mt_srand(fmod(time() * getmypid(), 0x7FFFFFFF) ^ fmod(1000000 * lcg_value(), 0x7FFFFFFF));
}

extract(unpack('Nrandom', pack('H*', sha1(mt_rand(0, 0x7FFFFFFF).$entropy.microtime()))));
return abs($random) % ($max - $min) + $min;

}

//==================================

function random_bytes($length) {

$bytes = '';

for($i = 0; $i < $length; $i++) $bytes.=chr(crypt_random(0, 255));

return $bytes;
}

//==================================

function create_secure_hash($password, $rounds=null) {

global $secure_hash_rounds;

if(is_null($rounds)) $rounds=$secure_hash_rounds;

$salt=random_bytes(16);
//16 bytes = 128 bits -- note that our salt is a binary salt

global $pepper;

$hash=hash('sha256', $pepper.$salt.$password, true);

$tmp17=pow(2, $rounds)-1;
while($tmp17--) {
$hash=hash('sha256', $hash.$password, true);
}

return $rounds.'*'.$salt.$hash;

}

//==================================

function verify_secure_hash($password, $hash) {

$rounds=substr($hash, 0, strpos($hash, '*'));

$salt=substr($hash, strpos($hash, '*')+1, strlen($hash)-strlen($rounds)-32-1);

$hash=substr($hash, strlen($hash)-32);

global $pepper;

$tmp17=$hash;

$hash=hash('sha256', $pepper.$salt.$password, true);

$rounds=pow(2, $rounds)-1;
while($rounds--) {
$hash=hash('sha256', $hash.$password, true);
}

return $tmp17===$hash;

}

//==================================

$password='test';

$password_hash=create_secure_hash($password);

echo 'Secure hash: ', $password_hash, '<br><br>';

if(verify_secure_hash($password, $password_hash)) echo 'Password ok.';
else echo 'Password wrong!';

?>

بنده میتونم بگم این یک روش نسبتا کامل و بحد کافی قوی هست. هم سالت رندوم داره هم سالت ثابت و هم key stretching و هم از توابع ضعیفی مثل md5 و sha1 استفاده نمیکنه (البته کاربردهای sha1 که در کد میبینید از اون کاربردهای خاص و استثناء هست که قبلا بهش اشاره کردم و در خودش الگوریتم هش مستقیما دخالتی نداره و اساس کارش نیست).

اما توابعی حتی از این امن تر هم وجود دارن که چون ممکنه هرجایی ساپورت نشن و از طرف دیگر استفاده از اونها رو درحال حاضر برای سطح و کاربردهای معمولی خودمون ضروری ندیدم، شخصا از اونها استفاده نکردم.
مثلا تابع bcrypt یک تابع مخصوص اینطور کارها و قویتره (البته سالت ثابت نداره که باید خودتون به پسورد قبل از هش کردن اضافه کنید)، یا حتی از اونهم خفن تر یک الگوریتمی طراحی شده بنام scrypt (اینم سالت ثابت نداره و کلا سالت ثابت رو خودتون باید اضافه کنید). ولی اینا هرجایی ساپورت نمیشن و روی پیاده سازیشون هم کار نکردم (فکر نمیکنم پیاده سازی آماده به زبان PHP ازشون پیدا بشه، و تازه اگر هم پیدا بشه بخاطر دلایل فنی (سرعت پایین) که روی امنیت هم تاثیر میذارن، یحتمل مناسب برای استفاده در کاربرد ما نخواهند بود).

توجه کنید که مقدار متغییر pepper رو باید بصورت دستی موقع نصب برنامه/راه اندازی سایت تغییر داد. مقدارش یک رشتهء رندوم 22 کاراکتری متشکل از اعداد و حروف انگلیسی بزرگ و کوچک باید باشه. اگر این متغییر رو تغییر بدید امنیت بالاتر میره. ولی باید توجه داشت که هش پسوردهای داخل دیتابیس به این متغییر وابسته هستند و اگر از دیتابیس بکاپ گرفته و ریستور بشه مقدار این متغییر هم باید همون مقداری باشه که هشهای موجود در دیتابیس باهاش محاسبه شدن، وگرنه کاربران نمیتونن لاگین کنن چون هشها مطابقت نخواهند کرد.

ضمنا درمورد تابع crypt_random باید بگم این یک تابع تولید اعداد رندوم امن هست (جایگزین امنی برای تابع mt_rand)، چون خود PHP یک تابع تولید اعداد رندوم که برای استفاده در کاربردهای رمزنگاری و امنیتی مناسب باشه نداره.
تازه سیستم تولید اعداد رندوم امن بنده یک بخش اضافهء ذخیرهء آنتروپی درخواستها رو هم داره که چون کد خودش رو داره و جدول و عملیات دیتابیس هم داره دیگه قاطی این کدها نکردم چون فکر میکنم خیلی ضروری نیست و همین کد برای کاربرد هش کردن پسورد و امنیت در سطح برنامه های موجود کفایت میکنه.

نکتهء دیگر اینکه خروجی تابع هش ما از نوع باینری است و بنابراین فیلدی در دیتابیس که برای ذخیرهء اون بکار میره باید از نوع binary یا var binary باشه، مگر اینکه هش رو قبل از درج در دیتابیس، توسط Base64 به ASCII تبدیل کنید (و اونوقت بعد از خوندنش از دیتابیس هم باید عکس همین عملیات رو انجام بدید تا بتونید هش رو Verify کنید).

نکتهء آخر هم اینکه امنیت نهایی هش به قوی بودن خود پسورد کاربر هم بستگی داره (در بدترین حالت، یعنی وقتی کرکر به تمام لایه های دیگر امنیت نفوذ کرده باشه، قدرت خود پسورد هم مهمه که پسورد واقعا ضعیفی نبوده باشه).

1 دیدگاه در “چگونه پسوردهای کاربران را به نحو امنی هش کنیم

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

*

شما می‌توانید از این دستورات HTML استفاده کنید: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>