วิธีใช้ password_hash ของ PHP เพื่อแฮชและยืนยันรหัสผ่าน


94

เมื่อเร็ว ๆ นี้ฉันได้พยายามใช้การรักษาความปลอดภัยของตัวเองบนสคริปต์การเข้าสู่ระบบที่ฉันพบบนอินเทอร์เน็ต password_hashหลังจากที่ดิ้นรนพยายามที่จะเรียนรู้วิธีที่จะทำให้สคริปต์ของตัวเองในการสร้างเกลือสำหรับผู้ใช้แต่ละคนที่ผมเจอ

จากสิ่งที่ฉันเข้าใจ (ตามออกของการอ่านในหน้านี้ ) password_hashเกลือจะถูกสร้างขึ้นแล้วในแถวเมื่อคุณใช้ นี่คือเรื่องจริง?

อีกคำถามหนึ่งที่ฉันมีคือจะไม่ฉลาดที่จะมี 2 เกลือ? หนึ่งโดยตรงในไฟล์และหนึ่งใน DB? ด้วยวิธีนี้หากมีคนประนีประนอมเกลือของคุณในฐานข้อมูลคุณยังมีอยู่ในไฟล์โดยตรงหรือไม่? ฉันอ่านที่นี่ว่าการเก็บเกลือไม่ใช่ความคิดที่ชาญฉลาด แต่ทำให้ฉันสับสนอยู่เสมอว่าผู้คนหมายถึงอะไร


8
ไม่ให้ฟังก์ชั่นดูแลเกลือ การใส่เกลือสองครั้งจะทำให้คุณมีปัญหาและไม่จำเป็นต้องทำ
Funk Forty Niner

คำตอบ:


182

การใช้password_hashเป็นวิธีที่แนะนำในการจัดเก็บรหัสผ่าน อย่าแยกเป็นฐานข้อมูลและไฟล์

สมมติว่าเรามีอินพุตต่อไปนี้:

$password = $_POST['password'];

คุณแฮชรหัสผ่านก่อนโดยทำสิ่งนี้:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

จากนั้นดูผลลัพธ์:

var_dump($hashed_password);

อย่างที่คุณเห็นมันถูกแฮช (ฉันถือว่าคุณทำตามขั้นตอนเหล่านั้นแล้ว)

ตอนนี้คุณเก็บรหัสผ่านแฮชนี้ในฐานข้อมูลของคุณจึงมั่นใจได้ว่าคอลัมน์รหัสผ่านของคุณมากพอที่จะถือค่าแฮช (อย่างน้อย 60 ตัวอักษรหรือนานกว่านั้น) เมื่อผู้ใช้ขอให้เข้าสู่ระบบให้คุณตรวจสอบการป้อนรหัสผ่านด้วยค่าแฮชนี้ในฐานข้อมูลโดยทำสิ่งนี้:

// Query the database for username and password
// ...

if(password_verify($password, $hashed_password)) {
    // If the password inputs matched the hashed password in the database
    // Do something, you know... log them in.
} 

// Else, Redirect them back to the login page.

อ้างอิงอย่างเป็นทางการ


2
โอเคฉันลองแค่นี้และได้ผล ฉันสงสัยในฟังก์ชั่นนี้เพราะมันดูเหมือนง่ายเกินไป คุณแนะนำให้ฉันสร้างความยาวของ varchar นานแค่ไหน? 225?
Josh Potter

4
สิ่งนี้มีอยู่แล้วในคู่มือphp.net/manual/en/function.password-hash.php --- php.net/manual/en/function.password-verify.phpซึ่ง OP อาจไม่ได้อ่านหรือเข้าใจ คำถามนี้ถูกถามบ่อยกว่าไม่มี
Funk Forty Niner

นั่นคือหน้าอื่น
Josh Potter

@JoshPotter ต่างจากอะไร? บวกกับสังเกตว่าพวกเขาไม่ได้ตอบคำถามที่ 2 ของคุณ พวกเขาอาจคาดหวังให้คุณค้นพบตัวเองหรือพวกเขาไม่รู้
Funk Forty Niner

8
@FunkFortyNiner, b / c Josh ถามคำถามฉันพบแล้ว 2 ปีต่อมาและมันช่วยฉันได้ นั่นคือประเด็นของ SO คู่มือนั้นชัดเจนราวกับโคลน
Toddmo

23

ใช่คุณเข้าใจถูกต้องแล้วฟังก์ชัน password_hash () จะสร้างเกลือขึ้นมาเองและรวมไว้ในค่าแฮชที่เป็นผลลัพธ์ การจัดเก็บเกลือในฐานข้อมูลนั้นถูกต้อง แต่ก็ทำงานได้แม้ว่าจะทราบ

// Hash a new password for storing in the database.
// The function automatically generates a cryptographically safe salt.
$hashToStoreInDb = password_hash($_POST['password'], PASSWORD_DEFAULT);

// Check if the hash of the entered login password, matches the stored hash.
// The salt and the cost factor will be extracted from $existingHashFromDb.
$isPasswordCorrect = password_verify($_POST['password'], $existingHashFromDb);

เกลือที่สองที่คุณพูดถึง (ที่เก็บไว้ในไฟล์) แท้จริงแล้วคือพริกไทยหรือคีย์ฝั่งเซิร์ฟเวอร์ ถ้าคุณใส่ก่อนแฮช (เช่นเกลือ) ให้ใส่พริกไทยลงไป มีวิธีที่ดีกว่าคือคุณสามารถคำนวณแฮชก่อนและหลังจากนั้นเข้ารหัสแฮช (สองทาง) ด้วยคีย์ฝั่งเซิร์ฟเวอร์ สิ่งนี้ช่วยให้คุณสามารถเปลี่ยนคีย์ได้เมื่อจำเป็น

ในทางตรงกันข้ามกับเกลือควรเก็บกุญแจนี้ไว้เป็นความลับ คนมักจะผสมมันและพยายามซ่อนเกลือ แต่จะดีกว่าถ้าปล่อยให้เกลือทำหน้าที่ของมันและเพิ่มความลับด้วยกุญแจ


8

ใช่มันเป็นความจริง. เหตุใดคุณจึงสงสัยคำถามที่พบบ่อยเกี่ยวกับฟังก์ชัน php :)

ผลของการวิ่งpassword_hash()มีสี่ส่วน:

  1. อัลกอริทึมที่ใช้
  2. พารามิเตอร์
  3. เกลือ
  4. แฮชรหัสผ่านจริง

อย่างที่คุณเห็นแฮชเป็นส่วนหนึ่งของมัน

แน่นอนว่าคุณอาจมีเกลือเพิ่มเติมสำหรับการรักษาความปลอดภัยที่เพิ่มขึ้นอีกชั้นหนึ่ง แต่ฉันคิดตามตรงว่ามันมากเกินไปในแอปพลิเคชัน php ทั่วไป อัลกอริทึม bcrypt เริ่มต้นนั้นดีและตัวเลือกปักเป้าที่เป็นตัวเลือกนั้นดีกว่า


2
bcrypt เป็นคร่ำเครียดฟังก์ชั่นในขณะปักเป้าเป็นอัลกอริทึมสำหรับการเข้ารหัส BCrypt มาจากอัลกอริทึม Blowfish แม้ว่า
martinstoeckli

7

อย่าใช้ md5 () ในการรักษารหัสผ่านของคุณแม้จะใส่เกลือก็ยังอันตรายเสมอ !!

ทำให้รหัสผ่านของคุณปลอดภัยด้วยอัลกอริทึมการแฮชล่าสุดดังต่อไปนี้

<?php

// Your original Password
$password = '121@121';

//PASSWORD_BCRYPT or PASSWORD_DEFAULT use any in the 2nd parameter
/*
PASSWORD_BCRYPT always results 60 characters long string.
PASSWORD_DEFAULT capacity is beyond 60 characters
*/
$password_encrypted = password_hash($password, PASSWORD_BCRYPT);

สำหรับการจับคู่กับรหัสผ่านที่เข้ารหัสของฐานข้อมูลและรหัสผ่านที่ผู้ใช้ป้อนให้ใช้ฟังก์ชันด้านล่าง

<?php 

if (password_verify($password_inputted_by_user, $password_encrypted)) {
    // Success!
    echo 'Password Matches';
}else {
    // Invalid credentials
    echo 'Password Mismatch';
}

หากคุณต้องการใช้เกลือของคุณเองให้ใช้ฟังก์ชันที่สร้างขึ้นเองสำหรับสิ่งเดียวกันเพียงทำตามด้านล่าง แต่ฉันไม่แนะนำให้ทำเช่นนี้เนื่องจากพบว่าเลิกใช้แล้วใน PHP เวอร์ชันล่าสุด

อ่านเกี่ยวกับpassword_hash ()ก่อนใช้โค้ดด้านล่าง

<?php

$options = [
    'salt' => your_custom_function_for_salt(), 
    //write your own code to generate a suitable & secured salt
    'cost' => 12 // the default cost is 10
];

$hash = password_hash($your_password, PASSWORD_DEFAULT, $options);

4
ตัวเลือกเกลือถูกเลิกใช้ด้วยเหตุผลที่ดีเนื่องจากฟังก์ชันนี้พยายามอย่างเต็มที่ในการสร้างเกลือที่ปลอดภัยในการเข้ารหัสและแทบจะเป็นไปไม่ได้เลยที่จะทำได้ดีกว่านี้
martinstoeckli

@martinstoeckli ใช่คุณพูดถูกฉันเพิ่งอัปเดตคำตอบขอบคุณ!
Mahesh Yadav

ถ้า (isset ($ _ POST ['btn-signup'])) {$ uname = mysql_real_escape_string ($ _ POST ['uname']); $ email = mysql_real_escape_string ($ _ POST ['อีเมล']); $ upass = md5 (mysql_real_escape_string ($ _ POST ['pass'])); นี่คือรหัสที่ใช้ใน login.php .. ฉันต้องการทำโดยไม่ใช้ Escape และ md5 ฉันต้องการใช้แฮชรหัสผ่าน ..
rashmi sm

PASSWORD_DEFAULT - ใช้อัลกอริทึม bcrypt (ต้องการ PHP 5.5.0) โปรดทราบว่าค่าคงที่นี้ได้รับการออกแบบให้เปลี่ยนแปลงอยู่ตลอดเวลาเนื่องจากอัลกอริทึมใหม่และแข็งแกร่งจะถูกเพิ่มเข้าไปใน PHP ด้วยเหตุนี้ความยาวของผลลัพธ์จากการใช้ตัวระบุนี้จึงสามารถเปลี่ยนแปลงได้ตลอดเวลา
Adrian P.

5

ไม่มีการอภิปรายอย่างชัดเจนเกี่ยวกับความเข้ากันได้แบบย้อนกลับและการส่งต่อที่สร้างขึ้นในฟังก์ชันรหัสผ่านของ PHP โดยเฉพาะ:

  1. ความเข้ากันได้ย้อนกลับ:ฟังก์ชันรหัสผ่านเป็นเครื่องห่อหุ้มที่เขียนไว้อย่างดีcrypt()และเข้ากันได้กับcrypt()แฮช - ฟอร์แมตโดยเนื้อแท้แม้ว่าจะใช้อัลกอริธึมแฮชที่ล้าสมัยและ / หรือไม่ปลอดภัยก็ตาม
  2. ความเข้ากันได้ของการส่งต่อ:การแทรกpassword_needs_rehash()และตรรกะเล็กน้อยลงในเวิร์กโฟลว์การตรวจสอบสิทธิ์ของคุณสามารถทำให้แฮชของคุณมีข้อมูลล่าสุดเกี่ยวกับอัลกอริทึมในปัจจุบันและในอนาคตโดยอาจมีการเปลี่ยนแปลงขั้นตอนการทำงานในอนาคต หมายเหตุ: สตริงใด ๆ ที่ไม่ตรงกับอัลกอริทึมที่ระบุจะถูกตั้งค่าสถานะว่าต้องการแฮชใหม่รวมถึงแฮชที่ไม่เข้ากันได้กับการเข้ารหัส

เช่น:

class FakeDB {
    public function __call($name, $args) {
        printf("%s::%s(%s)\n", __CLASS__, $name, json_encode($args));
        return $this;
    }
}

class MyAuth {
    protected $dbh;
    protected $fakeUsers = [
        // old crypt-md5 format
        1 => ['password' => '$1$AVbfJOzY$oIHHCHlD76Aw1xmjfTpm5.'],
        // old salted md5 format
        2 => ['password' => '3858f62230ac3c915f300c664312c63f', 'salt' => 'bar'],
        // current bcrypt format
        3 => ['password' => '$2y$10$3eUn9Rnf04DR.aj8R3WbHuBO9EdoceH9uKf6vMiD7tz766rMNOyTO']
    ];

    public function __construct($dbh) {
        $this->dbh = $dbh;
    }

    protected function getuser($id) {
        // just pretend these are coming from the DB
        return $this->fakeUsers[$id];
    }

    public function authUser($id, $password) {
        $userInfo = $this->getUser($id);

        // Do you have old, turbo-legacy, non-crypt hashes?
        if( strpos( $userInfo['password'], '$' ) !== 0 ) {
            printf("%s::legacy_hash\n", __METHOD__);
            $res = $userInfo['password'] === md5($password . $userInfo['salt']);
        } else {
            printf("%s::password_verify\n", __METHOD__);
            $res = password_verify($password, $userInfo['password']);
        }

        // once we've passed validation we can check if the hash needs updating.
        if( $res && password_needs_rehash($userInfo['password'], PASSWORD_DEFAULT) ) {
            printf("%s::rehash\n", __METHOD__);
            $stmt = $this->dbh->prepare('UPDATE users SET pass = ? WHERE user_id = ?');
            $stmt->execute([password_hash($password, PASSWORD_DEFAULT), $id]);
        }

        return $res;
    }
}

$auth = new MyAuth(new FakeDB());

for( $i=1; $i<=3; $i++) {
    var_dump($auth->authuser($i, 'foo'));
    echo PHP_EOL;
}

เอาท์พุต:

MyAuth::authUser::password_verify
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["$2y$10$zNjPwqQX\/RxjHiwkeUEzwOpkucNw49yN4jjiRY70viZpAx5x69kv.",1]])
bool(true)

MyAuth::authUser::legacy_hash
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["$2y$10$VRTu4pgIkGUvilTDRTXYeOQSEYqe2GjsPoWvDUeYdV2x\/\/StjZYHu",2]])
bool(true)

MyAuth::authUser::password_verify
bool(true)

หมายเหตุสุดท้ายเนื่องจากคุณสามารถแฮชรหัสผ่านของผู้ใช้ได้อีกครั้งเมื่อเข้าสู่ระบบคุณควรพิจารณา "ยกเลิก" แฮชเดิมที่ไม่ปลอดภัยเพื่อปกป้องผู้ใช้ของคุณ ด้วยเหตุนี้ฉันหมายความว่าหลังจากช่วงเวลาผ่อนผันบางอย่างคุณจะลบแฮชที่ไม่ปลอดภัยทั้งหมด [เช่น: MD5 / SHA / ที่อ่อนแออื่น ๆ ] และให้ผู้ใช้ของคุณพึ่งพากลไกการรีเซ็ตรหัสผ่านของแอปพลิเคชันของคุณ


0

รหัสผ่านคลาสรหัสเต็ม:

Class Password {

    public function __construct() {}


    /**
     * Hash the password using the specified algorithm
     *
     * @param string $password The password to hash
     * @param int    $algo     The algorithm to use (Defined by PASSWORD_* constants)
     * @param array  $options  The options for the algorithm to use
     *
     * @return string|false The hashed password, or false on error.
     */
    function password_hash($password, $algo, array $options = array()) {
        if (!function_exists('crypt')) {
            trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
            return null;
        }
        if (!is_string($password)) {
            trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
            return null;
        }
        if (!is_int($algo)) {
            trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
            return null;
        }
        switch ($algo) {
            case PASSWORD_BCRYPT :
                // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
                $cost = 10;
                if (isset($options['cost'])) {
                    $cost = $options['cost'];
                    if ($cost < 4 || $cost > 31) {
                        trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
                        return null;
                    }
                }
                // The length of salt to generate
                $raw_salt_len = 16;
                // The length required in the final serialization
                $required_salt_len = 22;
                $hash_format = sprintf("$2y$%02d$", $cost);
                break;
            default :
                trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
                return null;
        }
        if (isset($options['salt'])) {
            switch (gettype($options['salt'])) {
                case 'NULL' :
                case 'boolean' :
                case 'integer' :
                case 'double' :
                case 'string' :
                    $salt = (string)$options['salt'];
                    break;
                case 'object' :
                    if (method_exists($options['salt'], '__tostring')) {
                        $salt = (string)$options['salt'];
                        break;
                    }
                case 'array' :
                case 'resource' :
                default :
                    trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
                    return null;
            }
            if (strlen($salt) < $required_salt_len) {
                trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
                return null;
            } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
                $salt = str_replace('+', '.', base64_encode($salt));
            }
        } else {
            $salt = str_replace('+', '.', base64_encode($this->generate_entropy($required_salt_len)));
        }
        $salt = substr($salt, 0, $required_salt_len);

        $hash = $hash_format . $salt;

        $ret = crypt($password, $hash);

        if (!is_string($ret) || strlen($ret) <= 13) {
            return false;
        }

        return $ret;
    }


    /**
     * Generates Entropy using the safest available method, falling back to less preferred methods depending on support
     *
     * @param int $bytes
     *
     * @return string Returns raw bytes
     */
    function generate_entropy($bytes){
        $buffer = '';
        $buffer_valid = false;
        if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
            $buffer = mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM);
            if ($buffer) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
            $buffer = openssl_random_pseudo_bytes($bytes);
            if ($buffer) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid && is_readable('/dev/urandom')) {
            $f = fopen('/dev/urandom', 'r');
            $read = strlen($buffer);
            while ($read < $bytes) {
                $buffer .= fread($f, $bytes - $read);
                $read = strlen($buffer);
            }
            fclose($f);
            if ($read >= $bytes) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid || strlen($buffer) < $bytes) {
            $bl = strlen($buffer);
            for ($i = 0; $i < $bytes; $i++) {
                if ($i < $bl) {
                    $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
                } else {
                    $buffer .= chr(mt_rand(0, 255));
                }
            }
        }
        return $buffer;
    }

    /**
     * Get information about the password hash. Returns an array of the information
     * that was used to generate the password hash.
     *
     * array(
     *    'algo' => 1,
     *    'algoName' => 'bcrypt',
     *    'options' => array(
     *        'cost' => 10,
     *    ),
     * )
     *
     * @param string $hash The password hash to extract info from
     *
     * @return array The array of information about the hash.
     */
    function password_get_info($hash) {
        $return = array('algo' => 0, 'algoName' => 'unknown', 'options' => array(), );
        if (substr($hash, 0, 4) == '$2y$' && strlen($hash) == 60) {
            $return['algo'] = PASSWORD_BCRYPT;
            $return['algoName'] = 'bcrypt';
            list($cost) = sscanf($hash, "$2y$%d$");
            $return['options']['cost'] = $cost;
        }
        return $return;
    }

    /**
     * Determine if the password hash needs to be rehashed according to the options provided
     *
     * If the answer is true, after validating the password using password_verify, rehash it.
     *
     * @param string $hash    The hash to test
     * @param int    $algo    The algorithm used for new password hashes
     * @param array  $options The options array passed to password_hash
     *
     * @return boolean True if the password needs to be rehashed.
     */
    function password_needs_rehash($hash, $algo, array $options = array()) {
        $info = password_get_info($hash);
        if ($info['algo'] != $algo) {
            return true;
        }
        switch ($algo) {
            case PASSWORD_BCRYPT :
                $cost = isset($options['cost']) ? $options['cost'] : 10;
                if ($cost != $info['options']['cost']) {
                    return true;
                }
                break;
        }
        return false;
    }

    /**
     * Verify a password against a hash using a timing attack resistant approach
     *
     * @param string $password The password to verify
     * @param string $hash     The hash to verify against
     *
     * @return boolean If the password matches the hash
     */
    public function password_verify($password, $hash) {
        if (!function_exists('crypt')) {
            trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
            return false;
        }
        $ret = crypt($password, $hash);
        if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
            return false;
        }

        $status = 0;
        for ($i = 0; $i < strlen($ret); $i++) {
            $status |= (ord($ret[$i]) ^ ord($hash[$i]));
        }

        return $status === 0;
    }

}

0

ฉันได้สร้างฟังก์ชันที่ฉันใช้ตลอดเวลาสำหรับการตรวจสอบรหัสผ่านและเพื่อสร้างรหัสผ่านเช่นเก็บไว้ในฐานข้อมูล MySQL ใช้เกลือที่สร้างขึ้นแบบสุ่มซึ่งปลอดภัยกว่าการใช้เกลือแบบคงที่

function secure_password($user_pwd, $multi) {

/*
    secure_password ( string $user_pwd, boolean/string $multi ) 

    *** Description: 
        This function verifies a password against a (database-) stored password's hash or
        returns $hash for a given password if $multi is set to either true or false

    *** Examples:
        // To check a password against its hash
        if(secure_password($user_password, $row['user_password'])) {
            login_function();
        } 
        // To create a password-hash
        $my_password = 'uber_sEcUrE_pass';
        $hash = secure_password($my_password, true);
        echo $hash;
*/

// Set options for encryption and build unique random hash
$crypt_options = ['cost' => 11, 'salt' => mcrypt_create_iv(22, MCRYPT_DEV_URANDOM)];
$hash = password_hash($user_pwd, PASSWORD_BCRYPT, $crypt_options);

// If $multi is not boolean check password and return validation state true/false
if($multi!==true && $multi!==false) {
    if (password_verify($user_pwd, $table_pwd = $multi)) {
        return true; // valid password
    } else {
        return false; // invalid password
    }
// If $multi is boolean return $hash
} else return $hash;

}

6
ที่ดีที่สุดคือละเว้นsaltพารามิเตอร์ซึ่งจะถูกสร้างขึ้นโดยอัตโนมัติโดยฟังก์ชันpassword_hash ()ตามแนวทางปฏิบัติที่ดีที่สุด แทนที่จะPASSWORD_BCRYPTใช้PASSWORD_DEFAULTเพื่อเขียนรหัสพิสูจน์ในอนาคต
martinstoeckli

ขอบคุณสำหรับคำแนะนำที่ ฉันต้องดูแลมันในเอกสาร มันเป็นคืนที่ยาวนาน
Gerrit Fries

1
อ้างอิงจากsecure.php.net/manual/en/function.password-hash.php "ตัวเลือกเกลือได้เลิกใช้แล้วเมื่อ PHP 7.0.0 ตอนนี้ขอแนะนำให้ใช้เกลือที่สร้างขึ้นโดยค่าเริ่มต้น"
jmng
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.