Hooman.Prog
یک شنبه 01 آبان 1390, 22:22 عصر
اخیرا دارم روی کامل کردن امکانات و امنیت یکی از پروژه های تمرینی و قدیمی خودم که یک سیستم رجیستر و لاگین بود کار میکنم.
اول رفتم سروقت الگوریتم هش کردن پسوردهاش، چون هم از Salt استفاده نمیکنه و هم از الگوریتم md5 برای تولید هش استفاده میکنه که احتمالا میدونید که md5 از دیدگاه متخصصان رمزنگاری و امنیت مدتهاست شکسته شده بحساب میاد، اما هنوز عدهء زیادی بخصوص در PHP و بخصوص برای هش کردن پسورد کاربران ازش استفاده میکنن. ضمنا الگوریتم sha1 هم تقریبا وضعیت مشابه md5 رو داره! البته اینطور نیست که بشه این الگوریتم ها رو به این راحتی در بیشتر موارد شکست داد، ولی چون امنیت مسئلهء واقعا مهم و پیچیده ای است، باید به حرف متخصصان امنیت گوش کرد و استفاده از این الگوریتم ها رو در هر کاربردی کنار گذاشت.
خلاصه در نت جستجو کردم دنبال اینکه از چه الگوریتم و روش هشی برای پسورد کاربران استفاده کنم. خودم در فکر استفاده از sha256 یا sha512 بودم، اما به مطالب جالبی در نت برخوردم و به برنامه/الگوریتمی بنام bcrypt.
این الگوریتم توسط دو نفر بنامهای Niels Provos و David Mazières اختراع و در سال 1999 در USENIX معرفی شد.
بطور خلاصه میتونم بگم این الگوریتم مخصوص هش کردن پسورد کاربران ساخته شده و عمدا طوری طراحی شده که سنگین و کند باشه!
برای ساخت این الگوریتم از نسخه ای تغییریافته از الگوریتم رمزنگاری متقارن Blowfish بنام Eksblowfish استفاده شده.
نکته: با اینکه الگوریتم های هش الگوریتم هایی یک طرفه (بازگشتناپذیر) هستند اما میشه با روشهای مناسب، از الگوریتم های رمزنگاری بازگشتپذیر برای ساخت اونها استفاده کرد.
الگوریتم blowfish بخاطر روش تولید کلیدی که داره الگوریتمی سنگین و کند است، و نسخهء تغییر یافته از این الگوریتم بنام Eksblowfish که توسط Niels Provos و David Mazières طراحی شد عمدا حتی از blowfish هم سنگین تر و کندتره و در ضمن تعداد دورهای داخلی اون قابل تنظیم هست و بنابراین کندی اون میتونه با افزایش قدرت سخت افزارها یا نیاز به امنیت بالاتر تطبیق داده بشه.
این سنگین بودن و کندی باعث میشه کرکرها در کرک کردن پسوردهایی که با این روش هش شدن به مشکل بزرگی برخورد کنن که زندگی رو واقعا براشون سخت میکنه!
الگوریتم های دیگری مثل md5 و حتی sha256 از bcrypt بسیار بسیار سریعتر هستن و درواقع عمدا طوری طراحی شدن تا سرعت بالایی داشته باشن و نیز بتونن روی سخت افزارهای مخصوص انجام عملیات رمزنگاری براحتی و با سرعت بالا پیاده سازی و اجرا بشن؛ این خواص درمورد خیلی کاربردهای رمزنگاری مطلوب هستن، اما وقتی از این الگوریتم ها برای هش کردن پسورد کاربران استفاده میشه، عمدتا چون بسیاری از کاربران از پسوردهای ضعیفی استفاده میکنن، ویژگیهای نامطلوبی محسوب میشن که فقط به نفع کرکرها عمل میکنن.
و bcrypt نقطهء مقابل این ویژگیهاست! bcrypt باعث میشه کار کرک کردن حتی پسوردهای ضعیف خیلی مشکل تر بشه و پسوردهایی که کمی قوی تر باشن کم و بیش غیرقابل کرک شدن خواهند بود (مگر اینکه کرکر بتونه چند حدس معدود رو با موفقیت تست کنه - اما این دیگه brute-force نیست).
موضوع خیلی ساده است! برای کرک کردن هش های bcrypt نیاز به توان پردازشی بسیار بسیار بیشتری نسبت به هش های دیگر است. بنابراین کرک کردن (به روش brute-force) پسوردهای خیلی ضعیف تری در این روش غیرممکن خواهد شد که در روشهای دیگر قابل کرک هستند.
ضمنا توجه داشته باشید که این روش هم مثل بقیهء روشها از Salt استفاده میکنه. Salt یک روش ضروری برای امن کردن تمام هش ها محسوب میشه. اما یک خوبی دیگر این برنامه اینه که خودش Salt رو بصورت خودکار تولید میکنه و Salt در رشتهء خروجی گنجانده میشه؛ در نتیجه شما نیازی نیست به تولید و ذخیرهء جداگانهء Salt بپردازید؛ این برنامه تمام این کارها رو خودش انجام میده. شما فقط کافیه رشتهء خروجی از اون رو که شامل هش و Salt و یکسری مشخصات دیگه هست در دیتابیس ذخیره کنید.
توجه کنید که محاسبهء هش bcrypt برای ثبت نام یا لاگین کردن کاربران، حداقل با ترافیک های معمولی سرورها برای اینطور اعمال، فشار زیادی به سرور نمیاره، اما چون یک کرکر برای کرک موثر نیاز به محاسبهء هش های زیادی در زمان کوتاه داره، این الگوریتم پدرش رو درمیاره!!
خب اینم یک کلاس که الگوریتم هش bcrypt رو در PHP براحتی در اختیارمون میذاره:
<?php
class Bcrypt {
private $rounds;
public function __construct($rounds = 12) {
if(CRYPT_BLOWFISH != 1) {
throw new Exception("bcrypt not supported in this installation. See http://php.net/crypt");
}
$this->rounds = $rounds;
}
public function hash($input) {
$hash = crypt($input, $this->getSalt());
if(strlen($hash) > 13)
return $hash;
return false;
}
public function verify($input, $existingHash) {
$hash = crypt($input, $existingHash);
return $hash === $existingHash;
}
private function getSalt() {
$salt = sprintf('$2a$%02d$', $this->rounds);
$bytes = $this->getRandomBytes(16);
$salt .= $this->encodeBytes($bytes);
return $salt;
}
private $randomState;
private function getRandomBytes($count) {
$bytes = '';
if(function_exists('openssl_random_pseudo_bytes') &&
(strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win
$bytes = openssl_random_pseudo_bytes($count);
}
if($bytes === '' && is_readable('/dev/urandom') &&
($hRand = @fopen('/dev/urandom', 'rb')) !== FALSE) {
$bytes = fread($hRand, $count);
fclose($hRand);
}
if(strlen($bytes) < $count) {
$bytes = '';
if($this->randomState === null) {
$this->randomState = microtime();
if(function_exists('getmypid')) {
$this->randomState .= getmypid();
}
}
for($i = 0; $i < $count; $i += 16) {
$this->randomState = md5(microtime() . $this->randomState);
if (PHP_VERSION >= '5') {
$bytes .= md5($this->randomState, true);
} else {
$bytes .= pack('H*', md5($this->randomState));
}
}
$bytes = substr($bytes, 0, $count);
}
return $bytes;
}
private function encodeBytes($input) {
// The following is code from the PHP Password Hashing Framework
$itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx yz0123456789';
$output = '';
$i = 0;
do {
$c1 = ord($input[$i++]);
$output .= $itoa64[$c1 >> 2];
$c1 = ($c1 & 0x03) << 4;
if ($i >= 16) {
$output .= $itoa64[$c1];
break;
}
$c2 = ord($input[$i++]);
$c1 |= $c2 >> 4;
$output .= $itoa64[$c1];
$c1 = ($c2 & 0x0f) << 2;
$c2 = ord($input[$i++]);
$c1 |= $c2 >> 6;
$output .= $itoa64[$c1];
$output .= $itoa64[$c2 & 0x3f];
} while (1);
return $output;
}
}
?>
نکته: ظاهرا وجود ثابت CRYPT_BLOWFISH که در این کد چک میشه، به این معناست که تابع crypt الگوریتم Eksblowfish رو ساپورت میکنه و بنابراین میتونه برای هش کردن ازش استفاده کنه (تاجاییکه میدونم تابع crypt میتونه از الگوریتم های مختلفی برای تولید هش ها استفاده کنه).
روش استفاده رو در زیر نشون میدیم.
هش کردن:
<?php
include 'class_bcrypt.php';
$bcrypt = new Bcrypt();
$hash = $bcrypt->hash('password');
echo $hash;
?>
برنامهء بالا هشی مشابه این رو تولید میکنه (گفتم بذارم تا بدون تست کردن بتونید فرمتش رو بررسی کنید):
$2a$12$QdNt6GbsP1aWp9Jkhex4qepFumQlmtEXnfeWqU4rN7y mQygXZmtzy
و روش Verify کردن هش درمقابل پسورد وارد شده:
<?php
include 'class_bcrypt.php';
$bcrypt = new Bcrypt();
$hash = '$2a$12$QdNt6GbsP1aWp9Jkhex4qepFumQlmtEXnfeWqU4rN7 ymQygXZmtzy';
$isGood = $bcrypt->verify('password', $hash);
if($isGood) echo 'Password correct.';
else echo 'Password incorrect!';
?>
موقع ایجاد یک نمونه از کلاس Bcrypt میتونید تعداد round ها یا دورهای داخلی الگوریتم رو تعیین کنید. این تعداد بصورت پیشفرض روی 12 هست، ولی بطور مثال در کد زیر اون رو 15 تعیین میکنیم:
$bcrypt = new Bcrypt(15);
البته جای اشاره داره که تعداد round های داخلی در الگوریتم در اصل از محاسبهء مقدار 2 به توان عددی که شما مشخص میکند حاصل میشه، نه اینکه برابر با خود این عدد باشه.
کند بودن این الگوریتم موقع اجرا در مرورگر کاملا مشهود است. اگر مشخص نبود میتونید تعداد round رو زیاد کنید تا ببینید چقدر کنده! البته فکر نمیکنم نیازی باشه کندی اون اینقدر زیاد باشه که یک تاخیر مثلا چند ثانیه ای در مرورگر دیده بشه. احتمالا همون مقدار 12 خودش کاملا کافی هست.
اول رفتم سروقت الگوریتم هش کردن پسوردهاش، چون هم از Salt استفاده نمیکنه و هم از الگوریتم md5 برای تولید هش استفاده میکنه که احتمالا میدونید که md5 از دیدگاه متخصصان رمزنگاری و امنیت مدتهاست شکسته شده بحساب میاد، اما هنوز عدهء زیادی بخصوص در PHP و بخصوص برای هش کردن پسورد کاربران ازش استفاده میکنن. ضمنا الگوریتم sha1 هم تقریبا وضعیت مشابه md5 رو داره! البته اینطور نیست که بشه این الگوریتم ها رو به این راحتی در بیشتر موارد شکست داد، ولی چون امنیت مسئلهء واقعا مهم و پیچیده ای است، باید به حرف متخصصان امنیت گوش کرد و استفاده از این الگوریتم ها رو در هر کاربردی کنار گذاشت.
خلاصه در نت جستجو کردم دنبال اینکه از چه الگوریتم و روش هشی برای پسورد کاربران استفاده کنم. خودم در فکر استفاده از sha256 یا sha512 بودم، اما به مطالب جالبی در نت برخوردم و به برنامه/الگوریتمی بنام bcrypt.
این الگوریتم توسط دو نفر بنامهای Niels Provos و David Mazières اختراع و در سال 1999 در USENIX معرفی شد.
بطور خلاصه میتونم بگم این الگوریتم مخصوص هش کردن پسورد کاربران ساخته شده و عمدا طوری طراحی شده که سنگین و کند باشه!
برای ساخت این الگوریتم از نسخه ای تغییریافته از الگوریتم رمزنگاری متقارن Blowfish بنام Eksblowfish استفاده شده.
نکته: با اینکه الگوریتم های هش الگوریتم هایی یک طرفه (بازگشتناپذیر) هستند اما میشه با روشهای مناسب، از الگوریتم های رمزنگاری بازگشتپذیر برای ساخت اونها استفاده کرد.
الگوریتم blowfish بخاطر روش تولید کلیدی که داره الگوریتمی سنگین و کند است، و نسخهء تغییر یافته از این الگوریتم بنام Eksblowfish که توسط Niels Provos و David Mazières طراحی شد عمدا حتی از blowfish هم سنگین تر و کندتره و در ضمن تعداد دورهای داخلی اون قابل تنظیم هست و بنابراین کندی اون میتونه با افزایش قدرت سخت افزارها یا نیاز به امنیت بالاتر تطبیق داده بشه.
این سنگین بودن و کندی باعث میشه کرکرها در کرک کردن پسوردهایی که با این روش هش شدن به مشکل بزرگی برخورد کنن که زندگی رو واقعا براشون سخت میکنه!
الگوریتم های دیگری مثل md5 و حتی sha256 از bcrypt بسیار بسیار سریعتر هستن و درواقع عمدا طوری طراحی شدن تا سرعت بالایی داشته باشن و نیز بتونن روی سخت افزارهای مخصوص انجام عملیات رمزنگاری براحتی و با سرعت بالا پیاده سازی و اجرا بشن؛ این خواص درمورد خیلی کاربردهای رمزنگاری مطلوب هستن، اما وقتی از این الگوریتم ها برای هش کردن پسورد کاربران استفاده میشه، عمدتا چون بسیاری از کاربران از پسوردهای ضعیفی استفاده میکنن، ویژگیهای نامطلوبی محسوب میشن که فقط به نفع کرکرها عمل میکنن.
و bcrypt نقطهء مقابل این ویژگیهاست! bcrypt باعث میشه کار کرک کردن حتی پسوردهای ضعیف خیلی مشکل تر بشه و پسوردهایی که کمی قوی تر باشن کم و بیش غیرقابل کرک شدن خواهند بود (مگر اینکه کرکر بتونه چند حدس معدود رو با موفقیت تست کنه - اما این دیگه brute-force نیست).
موضوع خیلی ساده است! برای کرک کردن هش های bcrypt نیاز به توان پردازشی بسیار بسیار بیشتری نسبت به هش های دیگر است. بنابراین کرک کردن (به روش brute-force) پسوردهای خیلی ضعیف تری در این روش غیرممکن خواهد شد که در روشهای دیگر قابل کرک هستند.
ضمنا توجه داشته باشید که این روش هم مثل بقیهء روشها از Salt استفاده میکنه. Salt یک روش ضروری برای امن کردن تمام هش ها محسوب میشه. اما یک خوبی دیگر این برنامه اینه که خودش Salt رو بصورت خودکار تولید میکنه و Salt در رشتهء خروجی گنجانده میشه؛ در نتیجه شما نیازی نیست به تولید و ذخیرهء جداگانهء Salt بپردازید؛ این برنامه تمام این کارها رو خودش انجام میده. شما فقط کافیه رشتهء خروجی از اون رو که شامل هش و Salt و یکسری مشخصات دیگه هست در دیتابیس ذخیره کنید.
توجه کنید که محاسبهء هش bcrypt برای ثبت نام یا لاگین کردن کاربران، حداقل با ترافیک های معمولی سرورها برای اینطور اعمال، فشار زیادی به سرور نمیاره، اما چون یک کرکر برای کرک موثر نیاز به محاسبهء هش های زیادی در زمان کوتاه داره، این الگوریتم پدرش رو درمیاره!!
خب اینم یک کلاس که الگوریتم هش bcrypt رو در PHP براحتی در اختیارمون میذاره:
<?php
class Bcrypt {
private $rounds;
public function __construct($rounds = 12) {
if(CRYPT_BLOWFISH != 1) {
throw new Exception("bcrypt not supported in this installation. See http://php.net/crypt");
}
$this->rounds = $rounds;
}
public function hash($input) {
$hash = crypt($input, $this->getSalt());
if(strlen($hash) > 13)
return $hash;
return false;
}
public function verify($input, $existingHash) {
$hash = crypt($input, $existingHash);
return $hash === $existingHash;
}
private function getSalt() {
$salt = sprintf('$2a$%02d$', $this->rounds);
$bytes = $this->getRandomBytes(16);
$salt .= $this->encodeBytes($bytes);
return $salt;
}
private $randomState;
private function getRandomBytes($count) {
$bytes = '';
if(function_exists('openssl_random_pseudo_bytes') &&
(strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win
$bytes = openssl_random_pseudo_bytes($count);
}
if($bytes === '' && is_readable('/dev/urandom') &&
($hRand = @fopen('/dev/urandom', 'rb')) !== FALSE) {
$bytes = fread($hRand, $count);
fclose($hRand);
}
if(strlen($bytes) < $count) {
$bytes = '';
if($this->randomState === null) {
$this->randomState = microtime();
if(function_exists('getmypid')) {
$this->randomState .= getmypid();
}
}
for($i = 0; $i < $count; $i += 16) {
$this->randomState = md5(microtime() . $this->randomState);
if (PHP_VERSION >= '5') {
$bytes .= md5($this->randomState, true);
} else {
$bytes .= pack('H*', md5($this->randomState));
}
}
$bytes = substr($bytes, 0, $count);
}
return $bytes;
}
private function encodeBytes($input) {
// The following is code from the PHP Password Hashing Framework
$itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx yz0123456789';
$output = '';
$i = 0;
do {
$c1 = ord($input[$i++]);
$output .= $itoa64[$c1 >> 2];
$c1 = ($c1 & 0x03) << 4;
if ($i >= 16) {
$output .= $itoa64[$c1];
break;
}
$c2 = ord($input[$i++]);
$c1 |= $c2 >> 4;
$output .= $itoa64[$c1];
$c1 = ($c2 & 0x0f) << 2;
$c2 = ord($input[$i++]);
$c1 |= $c2 >> 6;
$output .= $itoa64[$c1];
$output .= $itoa64[$c2 & 0x3f];
} while (1);
return $output;
}
}
?>
نکته: ظاهرا وجود ثابت CRYPT_BLOWFISH که در این کد چک میشه، به این معناست که تابع crypt الگوریتم Eksblowfish رو ساپورت میکنه و بنابراین میتونه برای هش کردن ازش استفاده کنه (تاجاییکه میدونم تابع crypt میتونه از الگوریتم های مختلفی برای تولید هش ها استفاده کنه).
روش استفاده رو در زیر نشون میدیم.
هش کردن:
<?php
include 'class_bcrypt.php';
$bcrypt = new Bcrypt();
$hash = $bcrypt->hash('password');
echo $hash;
?>
برنامهء بالا هشی مشابه این رو تولید میکنه (گفتم بذارم تا بدون تست کردن بتونید فرمتش رو بررسی کنید):
$2a$12$QdNt6GbsP1aWp9Jkhex4qepFumQlmtEXnfeWqU4rN7y mQygXZmtzy
و روش Verify کردن هش درمقابل پسورد وارد شده:
<?php
include 'class_bcrypt.php';
$bcrypt = new Bcrypt();
$hash = '$2a$12$QdNt6GbsP1aWp9Jkhex4qepFumQlmtEXnfeWqU4rN7 ymQygXZmtzy';
$isGood = $bcrypt->verify('password', $hash);
if($isGood) echo 'Password correct.';
else echo 'Password incorrect!';
?>
موقع ایجاد یک نمونه از کلاس Bcrypt میتونید تعداد round ها یا دورهای داخلی الگوریتم رو تعیین کنید. این تعداد بصورت پیشفرض روی 12 هست، ولی بطور مثال در کد زیر اون رو 15 تعیین میکنیم:
$bcrypt = new Bcrypt(15);
البته جای اشاره داره که تعداد round های داخلی در الگوریتم در اصل از محاسبهء مقدار 2 به توان عددی که شما مشخص میکند حاصل میشه، نه اینکه برابر با خود این عدد باشه.
کند بودن این الگوریتم موقع اجرا در مرورگر کاملا مشهود است. اگر مشخص نبود میتونید تعداد round رو زیاد کنید تا ببینید چقدر کنده! البته فکر نمیکنم نیازی باشه کندی اون اینقدر زیاد باشه که یک تاخیر مثلا چند ثانیه ای در مرورگر دیده بشه. احتمالا همون مقدار 12 خودش کاملا کافی هست.