Hướng dẫn dùng rehash trong PHP

Gần đây tôi đã cố gắng thực hiện bảo mật của riêng mình trên một kịch bản đăng nhập mà tôi tình cờ thấy trên internet. Sau khi cố gắng học cách tạo tập lệnh của riêng tôi để tạo muối cho mỗi người dùng, tôi tình cờ tìm thấy password_hash.

Từ những gì tôi hiểu (dựa trên việc đọc trên trang này: http://php.net/manual/en/faq.passwords.php ), muối đã được tạo trong hàng khi bạn sử dụng mật khẩu_hash. Điều này có đúng không?

Một câu hỏi khác mà tôi có là, liệu có 2 muối không? Một trực tiếp trong tập tin và một trong DB? Bằng cách đó, nếu ai đó thỏa hiệp muối của bạn trong DB, bạn vẫn có trực tiếp trong tệp? Tôi đọc ở đây rằng lưu trữ muối không bao giờ là một ý tưởng thông minh, nhưng nó luôn làm tôi bối rối không biết mọi người có ý gì.

Sử dụng password_hash là cách lưu trữ mật khẩu được đề xuất. Đừng tách chúng thành DB và các tệp.

Giả sử chúng ta có đầu vào sau:

$password = $_POST['password'];

Tôi không xác nhận đầu vào chỉ để hiểu khái niệm này.

Trước tiên, bạn băm mật khẩu bằng cách này:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

Sau đó xem đầu ra:

var_dump($hashed_password);

Như bạn có thể thấy nó được băm. (Tôi giả sử bạn đã làm những bước đó).

Bây giờ bạn lưu trữ hashed_password này trong cơ sở dữ liệu của bạn, và sau đó giả sử khi người dùng yêu cầu đăng nhập chúng. Bạn kiểm tra mật khẩu nhập với giá trị băm này trong cơ sở dữ liệu, bằng cách thực hiện:

// 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.

Tham khảo chính thức

Có bạn đã hiểu chính xác, hàm password_hash () sẽ tự tạo một muối và bao gồm nó trong giá trị băm kết quả. Lưu trữ muối trong cơ sở dữ liệu là hoàn toàn chính xác, nó thực hiện công việc của mình ngay cả khi biết.

// 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);

Muối thứ hai mà bạn đề cập (một loại được lưu trữ trong một tệp), thực sự là một hạt tiêu hoặc một khóa bên máy chủ. Nếu bạn thêm nó trước khi băm (như muối), sau đó bạn thêm một hạt tiêu. Mặc dù vậy, có một cách tốt hơn, trước tiên bạn có thể tính toán hàm băm và sau đó mã hóa (hai chiều) hàm băm bằng khóa phía máy chủ. Điều này cung cấp cho bạn khả năng thay đổi chìa khóa khi cần thiết.

Ngược lại với muối, chìa khóa này nên được giữ bí mật. Mọi người thường trộn nó lên và cố gắng giấu muối, nhưng tốt hơn là để muối làm công việc của nó và thêm bí mật bằng một chìa khóa.

Vâng đúng vậy. Tại sao bạn nghi ngờ các faq php trên chức năng? :)

Kết quả của việc chạy password_hash() có bốn phần:

  1. thuật toán được sử dụng
  2. thông số
  3. muối
  4. băm mật khẩu thực tế

Vì vậy, như bạn có thể thấy, băm là một phần của nó.

Chắc chắn, bạn có thể có thêm một loại muối cho một lớp bảo mật bổ sung, nhưng tôi thành thật nghĩ rằng đó là quá mức cần thiết trong một ứng dụng php thông thường. Thuật toán bcrypt mặc định là tốt, và người thổi tùy chọn thậm chí còn tốt hơn.

Có một sự thiếu thảo luận rõ ràng về khả năng tương thích ngược và xuôi được tích hợp trong các chức năng mật khẩu của PHP. Đáng chú ý:

  1. Khả năng tương thích ngược: Các chức năng mật khẩu về cơ bản là một trình bao bọc được viết tốt xung quanh crypt() và vốn tương thích ngược với crypt()- định dạng băm, ngay cả khi chúng sử dụng thuật toán băm lỗi thời và/hoặc không an toàn.
  2. Chuyển tiếp tương thích: Chèn password_needs_rehash() và một chút logic vào quy trình xác thực của bạn có thể giúp bạn cập nhật thông tin hiện tại và các thuật toán trong tương lai với khả năng thay đổi trong tương lai đối với quy trình công việc. Lưu ý: Bất kỳ chuỗi nào không khớp với thuật toán đã chỉ định sẽ được gắn cờ vì cần phải thử lại, bao gồm cả băm không tương thích với mật mã.

Ví dụ:

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;
}

Đầu ra:

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)

Như một lưu ý cuối cùng, do bạn chỉ có thể băm lại mật khẩu của người dùng khi đăng nhập, bạn nên xem xét việc băm "không an toàn" để bảo vệ người dùng của mình. Điều này có nghĩa là sau một thời gian gia hạn nhất định, bạn sẽ xóa tất cả các băm không an toàn [ví dụ: MD5/SHA/nếu không yếu] và người dùng của bạn dựa vào các cơ chế đặt lại mật khẩu của ứng dụng.

Không bao giờ sử dụng md5 () để bảo mật mật khẩu của bạn, ngay cả với muối, nó luôn nguy hiểm !!

Làm cho mật khẩu của bạn được bảo mật với các thuật toán băm mới nhất như dưới đây.


Để khớp với mật khẩu được mã hóa của cơ sở dữ liệu và mật khẩu người dùng nhập vào, hãy sử dụng chức năng dưới đây.


Nếu bạn muốn sử dụng muối của riêng bạn, hãy sử dụng chức năng được tạo tùy chỉnh của bạn cho cùng, chỉ cần làm theo bên dưới, nhưng tôi không khuyến nghị điều này vì nó bị phản đối trong các phiên bản mới nhất của PHP.

đọc cái này http://php.net/manual/en/feft.password-hash.php trước khi sử dụng mã bên dưới.

 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);

?>

Hy vọng tất cả những điều này sẽ giúp !!

Mật khẩu lớp đầy đủ:

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;
    }

}