php MySql إنشاء فضاء للأعضاء

تشفير كلمة المرور bcrypt

آخر تحيين: 13-12-2015

php طرق تفعيل العضوية كلمة المرور بين التشفير و السرقة


الطريقة الصحيحة لتشفير كلمة المرور

لغة php وفّرت لنا مكتبة API كاملة لحماية كلمة المرور حماية صحيحة . تعتمد على خوارزمية bcrypt التي تُعتبر الأكثر أمانا "لحدود كتابتي هذه السطور" . تتلخص أهمية هذه المكتبة في 4 دوال تقوم بكل شيء نيابة عنكم : من تشفير و إضافة حبة الملح و مقارنة ...

  1. password_hash() – لتشفير كلمة المرور . نستعمل هذه الدالة أثناء معالجة بيانات استمارة التسجيل أو إستمارة تعديل كلمة المرور
  2. password_verify() – لمقارنة كلمة المرور التي أرسلها العضو ، بالتشفير المطابق لها في قاعدة البيانات . نستعملها أثناء معالجة بيانات استمارة الدّخول . أو في جميع الحالات التي تتطلب من العضو إدخال كلمة مروره للتعريف عن نفسه
  3. password_needs_rehash() – تستعمل في حالات اضطرارية ، عندما نحتاج تغيير تشفير كلمة المرور .
  4. password_get_info() – تعطينا معلومات عن التشفير المستخدم . هذه الدّالة لن نُقدّمها إلى الأعضاء ، بل نستعملها لحسابنا الشخصي ، نادرا ما ستحتاجونها و ربما لن تحتاجونها نهائيا .

قبل الذهاب بعيدا ، يجب أن تتأكدوا من إصدار php على خادومكم . لمعرفة الإصدار لديكم طرق عديدة ، نفذوا هذه الشيفرة مثلا
echo phpversion(); أو phpinfo();
يجب معرفة الإصدار لأن دوال كلمة المرور السابقة ، مُخصّصة لإصدار php 5.5 و الإصدارات الحديثة ، إن كان الأمر كذلك ، يمكنكم القفز مباشرة إلى طريقة استعمال الدالّة password_hash . أمّا إن كان إصداركم أقل من php 5.5 المرجو قراءة مايلي :

--------------- هذه الفقرة مخصصة فقط للإصدارات أقل من php 5.5 ---------------
بالنسبة لإصدارات php أكبر من PHP >= 5.3.7 . ستستفيدون أيضا من هذه المكتبة . لكن قبل ذلك يجب أن تُحمّلوا مكتبة داعمة لهذه الإصدارات .
لتحميل المكتبة لديكم خياران :

  • إذا كنتم تستعملون composer لتنصيب الملفات ، اتبعوا هذا الرابط Packagist ircmaxell password_compat . ثم قوموا بتنصيب الملف باتباع الخطوات الموجودة في هذه الصفحة : github ircmaxell password_compat .
  • إذا لم تسمعوا أبدا و لا تعرفون ماذا يعني Composer . يمكنكم تحميل الملف باتباع هذا الرابط : github ircmaxell password_compat
    ستجدون أيقونة التحميل : "Download Zip" على يمين الصفحة أسفل القائمة .
    بعد تحميل الملف في مكان ما في ملف موقعكم . قوموا بفك الضغط عنه .
    لاستعمال المكتبة ما عليكم سوى إقحام صفحة "password.php" في الصفحات التي تتطلب إجراء العمليات على كلمة المرور ، منها مثلا صفحة التسجيل و صفحة الدخول و صفحة تغيير كلمة المرور :
    <?php
    include 'lib/password_compat-master/lib/password.php';

    هذا كل ما في الأمر . يمكنكم متابعة الدرس .
    للتأكد إن كانت مكتبتكم تعمل بنجاح ، يمكنك عرض هذه الصفحة "version-test.php" الموجودة في ملف "lib" على متصفحكم . إن حصلتم على كلمة pass كما يلي : "Test for functionality of compat library: Pass" فالمكتبة تعمل بنجاح . و إلا أنصحكم بعد استعمالها كما ينص على ذلك صاحبها .
  • بالنسبة للذين يستعملون إصدارات php أقل من PHP 5.3.7 . لا يمكنكم استعمال هذه المكتبة ، نظرا لوجود ثغرة في استعمال bcrypt مع الإصدارات القديمة . و أحثكم على الإسراع بتحديث إصدار php لديكم .

    ----------------------------- هنا تنتهي فقرة الإصدارات أقل من 5.5 -----------------------------
    نعود إلى درسنا

password_hash

password_hash دالة سهلة الإستعمال ، نستعملها عندما نريد تشفير كلمة المرور . في حالتنا سنحتاجها أثناء معالجة بيانات استمارة التسجيل قبل تخزينها في قاعدة البيانات . ما علينا سوى إعطاءها كلمة المرور ، و ستقوم بتشفيرها و تمليحها بطريقة جد آمنة :

$hash_pass = password_hash($password, PASSWORD_DEFAULT);

الحجة الأولى التي أعطيناها للدالة = "password$" هي كلمة السر التي ستحصلون عليها غالبا من الإستمارة $_POST['pass'] . و الحجة الثانية "PASSWORD_DEFAULT" هي طريقة التشفير التي سنختارها . مبدئيا خوارزمية bcrypt هي المستعملة . يمكنكم أيضا كتابتها بهذه الطريقة :

$hash_pass = password_hash($password, PASSWORD_BCRYPT);

إذا استعملتم PASSWORD_BCRYPT .فحقل كلمة المرور في قاعدة البيانات لن يتجاوز 60 مكونا . varchar(60) .
بما أن PASSWORD_DEFAULT مبدئيا تستعمل bcrypt حاليا . لكن ، يمكن للغة php لاحقا في المستقبل أن تستعمل خوارزمية أخرى أكثر قوة و أمانا بدل bcrypt ، و هذا أمر وارد . لهذا أنصحكم باستعمال PASSWORD_DEFAULT حتى تستفيدوا من التحديث ، دون حتى علمكم بذلك . في هذه الحالة يجب أن يتجاوز حقل كلمة المرور في قاعدة البيانات 60 مُكوّناً ، استعملوا الحد الأقصى ، لهذا اخترنا في بداية الدّرس أثناء إنشاء جدول الأعضاء حقل بvarchar(255) . حتى إذا كان التشفير الجديد يتجاوز 60 مكونا لن تكون لديكم مشكلة . هذا كل شيء فيما يخصكم .
للذين يستعملون أرضيات العمل "Framework" مثل symfony2 أو Zend . من الأفضل استعمال PASSWORD_BCRYPT بدل PASSWORD_DEFAULT . الأرضيات في إصداراتها الأخيرة ، تأخذ بعين الإعتبار تشفير bcrypt . لأنه قد يتم تغيير التشفير من طرف php و ربما لن يتغير على أرضية عملكم أو في كل الحالات سيأخذون وقتا لتحديثه .

لزيادة الحماية لتشفيرنا ، سنلعب على معيار وقت التنفيذ cost. كنا رأينا في الدرس السابق ، أن وقت تنفيذ التشفير ، كلّما كان سريعا كلما كبرت فرص المهاجم لإيجاده . لهذا سنقوم بالزيادة في إبطائه حسب حاجتنا . هذا المعيار مبدئيا يساوي "10" : أي أنه إذا لم تُحدّدوه أثناء التشفير ، ستُشفّر كلمة المرور باستعمال "10" تلقائيا . لكننا غالبا ما نودّ رفع هذه القيمة . لهذا أتاحت لنا php هذه الميزة ، باستعمال المعيار "cost" بحيث يمكننا إعطائه أرقاما من "4" إلى "31" و نستعمله بهذه الطريقة :

$options = array("cost" => 11); 
$hash_pass = password_hash($password, PASSWORD_DEFAULT, $options);

كلما رفعنا قيمة cost بواحد ، كلما تضاعف (x2) الوقت المستغرق للتشفير و من ثم ازدادت حماية كلمة المرور . لهذا يجب رفع هذه القيمة قدر المستطاع مع عدم المبالغة . حتى لا ينتظر المستخدم دهرا لتتم عملية التشفير .
صممت السكريبت أسفله ، خصيصا لكم لإجراء التجارب و معرفة الوقت المستغرق لبعض القيم . في كل مرة ، يقوم السكريبت برفع قيمة cost . قوموا بنقل السكريبت و نسخه في صفحة test.php مثلا . و قوموا بتنفيذه على المتصفح :

<?php
$password = 'mypassword';
for ($i = 11; $i < 14; $i++) {
   $options = array("cost" => $i);
   $start_runtime = microtime(true);
   password_hash($password, PASSWORD_DEFAULT, $options);
   $end_runtime = microtime(true);
   // احتساب الوقت المستغرق
   $total_runtime = $end_runtime - $start_runtime;
   
   echo "<p>cost = ".$options['cost']; 
   echo "<br>الوقت المستغرق هو : ".round($total_runtime,2)." ثانية</p>";
}
?>

اختاروا قيمة cost المناسبة لكم . حسب الوقت المستغرق .
للمجتهدين الذين تابعوا الدّرس السابق . لا بد أنكم تساءلتم لماذا لم نتحدّث عن "حبة الملح" رغم أهميتها ؟
حبة الملح مثلها مثل cost ، فهي تضاف تلقائيا ، أما إذا أردتم تغييرها ، و هذا ما لن أنصحكم به . يجب أن لا تستعملوا نصا قارا كما رأينا في مثال للدّرس السابق ، يمكنكم مثلا إضافتها كالتالي :

<?php 
$options = array (
    'cost' => 11,
    'salt' => mcrypt_create_iv(22, MCRYPT_DEV_URANDOM),
);
$hash_pass = password_hash($password, PASSWORD_DEFAULT, $options);
// ...

لا ينصح البتة بتغيير قيمة حبة الملح "salt" المبدئية . php أخذ هذه المسؤولية على عاتقه ، فهو يقوم بإضافة حبة ملح محمية و وحيدة لكل كلمة مرور . لهذا ، إكتفوا بتغيير معيار cost فقط .

هذا المثال العام أسفله يعطيكم نظرة عن طريقة استعمال password_hash ، في صفحة معالجة بيانات التسجيل . لتشفير كلمة المرور قبل تخزينها في قاعدة البيانات
registration.php

<?php 
if(isset($_POST['submit'])) {
    // ...
        $password = $_POST['pass'];
    // يجب التأكد من البيانات
    // ثم تشفير كلمة المرور
    $options = array("cost" => 12);
    $hash_pass = password_hash($password, PASSWORD_DEFAULT, $options);
// تخزين البيانات في القاعدة
   

password_verify

سنحتاج هذه الدالة ، عندما نريد مقارنة كلمة المرور الحالية مع تشفيرها الموجود في قاعدة البيانات . في مثالنا سنُوظّفها أثناء معالجة استمارة الدخول "login.php" . بعد أن يملأ العضو الإستمارة ثم يرسلها ، قبل ربط الإتصال سنتأكد من أن كلمة المرور التي أدخلها صحيحة .

<?php
//...
$password = $_POST['pass'];
$pseudo = $_POST['pseudo'];
/** $db_password : كلمة المرور المشفّرة من قاعدة البيانات
يجب ربط الإتصال بقاعدة البيانات و أخذ كلمة المرور اعتمادا على إسم العضو 
**/
// التحقق من تطابق كلمتي المرور 
if (password_verify($password, $db_password)) {
    echo 'تم ربط الإتصال بنجاح '; 
} else {
    echo 'الإسم أو كلمة المرور خاطئة';
}
?>

العملية جد سهلة ، لن تحتاجوا إلى تشفير أو إضافة cost أو salt . كل شيء تقوم به password_verify نيابة عنكم .

password_needs_rehash

هذه الدالة ، تُتيح لنا إمكانية تغيير التشفير المعتمد ، أو تغيير قيمة COST
ـ لنفترض أنك استعملت سابقا إحدى طرق CRYPT للتشفير . و أردت الآن تغييره إلى BCRYT . لكنك في نفس الوقت لا تريد أن يفقد الأعضاء القُدامى كلمات مرورهم .
ـ أو أردت الإستفادة من التحديث ، دون حتى أن تعلم بذلك ، في حالة قامت لغة php بتغيير خوارزمية التشفير
ـ أو أردت فقط تغيير قيمة cost ، مع مرور الوقت ، ارتأيت مثلاأن ترفع من قيمتها .

لهذا يجب تغيير و تحيين التشفير القديم لجميع الأعضاء في قاعدة البيانات . لن نفعل هذا يدويا . فالدالة password_needs_rehash وُجدت لتسهل علينا المأمورية ، و ستقوم باعتماد التشفير الجديد لأي عضو بمجرد أن يملأ هذا الأخير استمارة الدخول و إرسالها .
لهذا سنضيفها في استمارة الدّخول "login.php" كالتالي :

<?php 
// ...
/**
 * $db_password = كلمة المرور المشفرة ، مأخوذة من قاعدة البيانات اعتمادا على إسم العضو
**/
if (password_verify($_POST['pass'], $db_password)) {
    //cost قمت برفع قيمة .
    // إذا كنتم لا تنوون تغييرها ، فلا جدوى من إضافتها
    $options = array("cost" => 13);
    // ثم نتأكد إن كان التشفير يحتاج إلى تحيين
    if (password_needs_rehash($db_password, PASSWORD_DEFAULT, $options)) {
        // نقوم بإعادة تشفير كلمة المرور وفقا للمعايير الجديدة
        $new_db_password = password_hash($_POST['pass'], PASSWORD_DEFAULT, $options);
        //  ثم نقوم بتحيين التشفير الجديد لكلمة المرور  في قاعدة البيانات 
   }
   // إنشاء متغيرات الجلسة 
}

مباشرة بعد أن ينجح العضو في دخوله ، تقوم الدالة password_needs_rehash() بالتأكد إن كانت كلمة مروره تحتاج إلى إعادة التشفير ، أو إلى تحيين لقيمة cost . إن كان الأمر كذلك ، نقوم بتحديث التشفير باستعمال الدالة المعروفة password_hash()

إذا سبق أن استضفتم موقعكم على الويب ، و قمتم سابقا باستعمال إحدى الطرق مثل sah256 أو md5 ... لشفير كلمات المرور . و أردتم الآن استعمال bcryt . في هذه الحالة ، الدالة password_needs_rehash لن تنفعكم في شيء . يبقى الحل هو إخبار الأعضاء بتغيير كلمات مرورهم حتى تتلائم مع التشفير الجديد . تجدون الحل باتباع هذا الرابط
أما إذا كنتم قد استعملتم إحدى طرق crypt للتشفير ، في هذه الحالة ستفيدكم الدالة password_needs_rehash كثيرا . بحيث لن تقوموا بفعل أي شيء ، في أول مرة سيقوم العضو بالولوج إلى حسابه ، سيتم تغيير تشفير كلمة مروره إلى bcryt . دون أن يفقد كلمة مروره أو أن يكون مضظرا لتغييرها .

password_get_info

$password = 'mypassword';
$options = array("cost" => 12);
$hash_pass = password_hash($password, PASSWORD_DEFAULT, $options);
echo '<pre>';
   print_r(password_get_info($hash_pass));
echo '</pre>';

النتيجة :


Array
(
    [algo] => 1
    [algoName] => bcrypt
    [options] => Array
        (
            [cost] => 12
        )
)


للذين ألفوا استعمال إحدى الطرق الكلاسيكية للتشفير و إضافة حبة الملح . غالبا هذه المهمة كانت تتطلب حقلين في قاعدة البيانات : حقل لتخزين تشفير كلمة المرور و حقل لتخزين حبة الملح . مع password_hash نحتاج لحقل واحد فقط لتخزينهما .
الصورة أسفله ، تُوضّح لكم جميع مُكونات التشفير الذي اعتمدناه :



المدونة : تفعيل صفحتي التسجيل و الدخول


كما جرت العادة ، لن أبخل عليكم بإهدائكم مثالا لتفعيل كل ما رأيناه في هذا الدرس . لهذا أدعوكم لإنشاء ملف جديد على موقعنا أو مدونتنا السابقة . لنسمّي هذا الملف "members" . و داخله سنضيف صفحات التسجيل و الدخول و الخروج :

registration.php

<?php 
require '../includes/header.php';
require '../includes/db-connection.php';

$error = array();
$user = null;
$email = null;

if (isset($_POST['submit']))
{
    $user  = $_POST['pseudo'];
    $email = $_POST['email'];
    $password = $_POST['pass'];
    $pass_confirm = $_POST['pass_confirm'];

    // معالجة بيانات الإستمارة
    if(empty($user)) { $error[] = 'حقل الإسم فارغ';}
    if(empty($email)) { $error[] = 'حقل البريد الإلكتروني فارغ';}
    if(empty($password)) { $error[] = 'حقل كلمة المرور  فارغ';}
    if(empty($pass_confirm)) { $error[] = 'حقل تأكيد كلمة المرور  فارغ';}

    if($password !== $pass_confirm) { $error[] = 'حقلي كلمة المرور و تأكيد المرور  غير متطابقين';}
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $error[] = " البريد الإلكتروني غير صحيح";}

    $response= $db->prepare('SELECT user_name, user_email
                   FROM blog_users
                  ');
    $response->bindValue(':nom',$user,PDO::PARAM_STR);
    $response->execute();
    $members = $response->FetchAll();
    $response->CloseCursor();

    if($response->rowCount() > 0)
    { 
      foreach ($members as $member) {
        if($member['user_name'] === $_POST['pseudo']) {
          $error[] = ' الإسم موجود مسبقا ، المرجو تغييره';
        }

        if($member['user_email'] === $_POST['email']) {
          $error[] = ' البريد الإلكتروني موجود مسبقا المرجو تغييره';
        }
      }
    }

    if(empty($error))
    {
        // تعيين معيار الوقت المستغرق للتشفير
        $options = array("cost" => 12);
        // تشفير كلمة المرور
        $hash_pass = password_hash($password, PASSWORD_DEFAULT, $options);

        // تخزين البيانات في القاعدة 
        $stmt= $db->prepare('INSERT INTO blog_users (user_name, user_email, user_password) 
                  VALUES (:name, :mail, :pass)
                ');
        $stmt->bindValue(':name',$user,PDO::PARAM_STR);
        $stmt->bindValue(':mail',$email,PDO::PARAM_STR);
        $stmt->bindValue(':pass',$hash_pass,PDO::PARAM_STR);
        $stmt->execute();

        $stmt->CloseCursor();

        // تعيين جلسة لإشعار المستخدم بنجاح عملية تسجيله ، ثم تحويله لصفحة الدخول
        $_SESSION['message'] = htmlspecialchars($user). ' : تم تسجيلك بنجاح ، يمكنك الولوج لحسابك عبراستمارة الدخول أسفله : ';
        header('location:login.php'); exit;
    }
    else 
    { // عرض الأخطاء الناجمة
        echo '<div class="errors"><ol>';
        foreach ($error as $value)
        {
            echo '<li>'.$value.'</li>';
        }
        echo '</ol></div>';
    }
}
?>
<form action="" id="form" method="post" >
    <fieldset class="form-item">
	    <legend>التسجيل</legend>
        <label for="pseudo">الإسم</label><input type="text" name="pseudo" id="pseudo" value="<?php echo htmlspecialchars($user);?>"><br>
        <label for="email">البريد الإلكتروني</label><input type="email" name="email" id="email" value="<?php echo htmlspecialchars($email);?>"><br>
        <label for="pass">كلمة المرور</label><input type="password" name="pass" id="pass"><br>
        <label for="pass_confirm">تأكيد كلمة المرور</label><input type="password" name="pass_confirm" id="pass_confirm"><br>
    </fieldset>
    <fieldset class="form-submit">
        <input type="submit" name="submit" value="موافق" class="button green">
    </fieldset>
</form>
<?php
require '../includes/footer.php';

login.php

<?php 
require '../includes/header.php';
require '../includes/db-connection.php';

// معالجة بيانات استمارة الدخول
$error = array();
$pseudo = null;

if (isset($_POST['pseudo']) AND isset($_POST['pass']))
{
    $pseudo = $_POST['pseudo'];
    $password = $_POST['pass'];
	
    if(empty($pseudo)) { $error[] = 'حقل الإسم فارغ';}
    if(empty($password)) { $error[] = 'حقل كلمة المرور  فارغ';}

   // أخذ البيانات من القاعدة إعتمادا على إسم العضو 
    $response= $db->prepare('SELECT user_id, user_name, user_password
                   FROM blog_users
                   WHERE user_name = :nom
                  ');
    $response->bindValue(':nom', $pseudo, PDO::PARAM_STR);
    $response->execute();
    $member = $response->Fetch();
    $response->CloseCursor();

    $db_password = $member['user_password'];

    // مقارنة كلمة المرور المرسلة بالموجودة في قاعدة البيانات	
    if (!password_verify($password, $db_password)) {
	    $error[] = 'الإسم و\أو كلمة المرور غير صحيحين';
    }

    if(empty($error)) 
    {
        // قيمة معيار وقت التنفيذ يجب أن تكون مطابقة للقيمة المحددة في صفحة التسجيل
        $options = array("cost" => 12);

        // إذا كانت كلمة المرور تحتاج لتحيين تشفيرها
        if (password_needs_rehash($db_password, PASSWORD_DEFAULT, $options)) {
            // تحديث التشفير
            $new_db_password = password_hash($password, PASSWORD_DEFAULT, $options);
            // تحيين التشفير في القاعدة
            $stmt= $db->prepare('UPDATE blog_users SET user_password = :pass WHERE user_name = :name');
            $stmt->bindValue(':pass', $new_db_password,PDO::PARAM_STR);
            $stmt->bindValue(':name', $pseudo,PDO::PARAM_STR);
            $stmt->execute();
            $stmt->CloseCursor();
        }
        
        // تعيين متغيرات جلسة العضو
        $_SESSION['id'] = $member['user_id'];
        $_SESSION['user'] = $member['user_name'];
		
        // ثم تحويله تلقائيا إلى أي صفحة نريد 
        header('location:../index.php');exit;
    }
    else 
    { // عرض الأخطاء الناجمة
        echo '<div class="errors"><ol>';
        foreach ($error as $value)
        {
            echo '<li>'.$value.'</li>';
        }
        echo '</ol></div>';
    }
}
?>
<form action="" method="post" id="form">
    <fieldset class="form-item">
        <legend>الدّخول</legend>
        <label for="pseudo">الإسم</label><input type="text" name="pseudo" id="pseudo" value="<?php echo htmlspecialchars($pseudo);?>"><br>
        <label for="pass">كلمة المرور</label><input type="password" name="pass" id="pass">
    </fieldset>
    <fieldset class="form-submit">
        <input type="submit" value="موافق" class="button green">
    </fieldset>
    </form>
<?php
require '../includes/footer.php';