Cara menggunakan password_hash PHP untuk melakukan hash dan memverifikasi kata sandi

94

Baru-baru ini saya mencoba menerapkan keamanan saya sendiri pada skrip log in yang saya temukan di internet. Setelah berjuang untuk mencoba mempelajari cara membuat skrip saya sendiri untuk menghasilkan garam untuk setiap pengguna, saya menemukan password_hash.

Dari apa yang saya pahami (berdasarkan bacaan di halaman ini ), garam sudah dihasilkan di baris saat Anda menggunakan password_hash. Apakah ini benar?

Pertanyaan lain yang saya miliki adalah, bukankah pintar memiliki 2 garam? Satu langsung di file dan satu di DB? Dengan begitu, jika seseorang mengkompromikan garam Anda di DB, Anda masih memiliki garam langsung di file? Saya membaca di sini bahwa menyimpan garam bukanlah ide yang cerdas, tetapi selalu membingungkan saya apa yang dimaksud orang dengan itu.

Josh Potter
sumber
8
Tidak. Biarkan fungsi menjaga garam. Penggaraman ganda akan membuat Anda kesulitan dan tidak perlu.
Funk Forty Niner

Jawaban:

182

Penggunaan password_hashadalah cara yang disarankan untuk menyimpan kata sandi. Jangan pisahkan mereka ke DB dan file.

Katakanlah kita memiliki masukan berikut:

$password = $_POST['password'];

Pertama-tama Anda mencirikan kata sandi dengan melakukan ini:

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

Kemudian lihat hasilnya:

var_dump($hashed_password);

Seperti yang Anda lihat, itu hash. (Saya berasumsi Anda melakukan langkah-langkah itu).

Sekarang Anda menyimpan kata sandi berciri ini di database Anda, memastikan kolom kata sandi Anda cukup besar untuk menampung nilai hash (setidaknya 60 karakter atau lebih) . Saat pengguna meminta untuk memasukkannya, Anda memeriksa input kata sandi dengan nilai hash ini di database, dengan melakukan ini:

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

Referensi Resmi

Akar
sumber
2
Oke, saya baru saja mencoba ini dan berhasil. Saya meragukan fungsinya karena sepertinya terlalu mudah. Berapa lama Anda merekomendasikan saya membuat panjang varchar saya? 225?
Josh Potter
4
Ini sudah ada di manual php.net/manual/en/function.password-hash.php --- php.net/manual/en/function.password-verify.php yang mungkin tidak dibaca atau dipahami oleh OP. Pertanyaan ini lebih sering ditanyakan daripada tidak sama sekali.
Funk Forty Niner
Itu halaman yang berbeda.
Josh Potter
@JoshPotter berbeda dari apa? plus, perhatikan bahwa mereka belum menanggapi pertanyaan kedua Anda. mereka mungkin mengharapkan Anda untuk mencari tahu sendiri, atau mereka tidak tahu.
Funk Forty Niner
8
@FunkFortyNiner, b / c Josh mengajukan pertanyaan, saya menemukannya, 2 tahun kemudian, dan itu membantu saya. Itulah inti dari SO. Manual itu sejelas lumpur.
toddmo
23

Ya, Anda memahaminya dengan benar, fungsi password_hash () akan menghasilkan garam sendiri, dan memasukkannya ke dalam nilai hash yang dihasilkan. Menyimpan salt dalam database benar-benar benar, ia melakukan tugasnya meskipun diketahui.

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

Salt kedua yang Anda sebutkan (yang disimpan dalam file), sebenarnya adalah lada atau kunci sisi server. Jika Anda menambahkannya sebelum hashing (seperti garam), maka Anda menambahkan merica. Namun ada cara yang lebih baik, Anda dapat menghitung hash terlebih dahulu, dan setelah itu mengenkripsi (dua arah) hash dengan kunci sisi server. Ini memberi Anda kemungkinan untuk mengubah kunci bila perlu.

Berbeda dengan garam, kunci ini harus dirahasiakan. Orang sering mencampurnya dan mencoba menyembunyikan garamnya, tetapi lebih baik membiarkan garam itu bekerja dan menambahkan rahasianya dengan kunci.

martinstoeckli.dll
sumber
8

Ya itu benar. Mengapa Anda meragukan faq php tentang fungsinya? :)

Hasil lari password_hash()memiliki empat bagian:

  1. algoritma yang digunakan
  2. parameter
  3. garam
  4. hash kata sandi sebenarnya

Jadi seperti yang Anda lihat, hash adalah bagian darinya.

Tentu, Anda dapat memiliki garam tambahan untuk lapisan keamanan tambahan, tapi sejujurnya saya pikir itu berlebihan dalam aplikasi php biasa. Algoritme bcrypt default bagus, dan blowfish opsional bisa dibilang lebih baik.

Joel Hinz
sumber
2
BCrypt adalah fungsi hashing , sedangkan Blowfish adalah algoritma untuk enkripsi . BCrypt berasal dari algoritma Blowfish.
martinstoeckli
7

Jangan pernah menggunakan md5 () untuk mengamankan kata sandi Anda, bahkan dengan salt, itu selalu berbahaya !!

Amankan sandi Anda dengan algoritme hashing terbaru seperti di bawah ini.

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

Untuk mencocokkan dengan kata sandi terenkripsi database dan kata sandi yang dimasukkan pengguna, gunakan fungsi di bawah ini.

<?php 

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

Jika Anda ingin menggunakan salt Anda sendiri, gunakan fungsi yang dibuat khusus untuk hal yang sama, cukup ikuti di bawah ini, tetapi saya tidak merekomendasikan ini karena hal ini sudah usang di versi terbaru PHP.

Baca tentang password_hash () sebelum menggunakan kode di bawah ini.

<?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);
Mahesh Yadav
sumber
4
Opsi garam tidak digunakan lagi karena alasan yang baik, karena fungsinya melakukan yang terbaik untuk menghasilkan garam yang aman secara kriptografik, dan hampir tidak mungkin untuk melakukannya dengan lebih baik.
martinstoeckli
@martinstoeckli, ya Anda benar, saya baru saja memperbarui jawaban saya, Terima kasih!
Mahesh Yadav
if (isset ($ _ POST ['btn-signup'])) {$ uname = mysql_real_escape_string ($ _ POST ['uname']); $ email = mysql_real_escape_string ($ _ POST ['email']); $ upass = md5 (mysql_real_escape_string ($ _ POST ['pass'])); Ini adalah kode yang digunakan di login.php .. saya ingin melakukannya tanpa menggunakan escape dan md5. saya ingin menggunakan hash password ..
rashmi sm
PASSWORD_DEFAULT - Gunakan algoritma bcrypt (Memerlukan PHP 5.5.0). Perhatikan bahwa konstanta ini dirancang untuk berubah seiring waktu karena algoritme baru dan lebih kuat ditambahkan ke PHP. Oleh karena itu, lamanya hasil dari penggunaan pengenal ini dapat berubah seiring waktu.
Adrian P.
5

Ada sedikit diskusi tentang kompatibilitas maju dan mundur yang dibangun di dalam fungsi kata sandi PHP. Terutama:

  1. Kompatibilitas Mundur: Fungsi kata sandi pada dasarnya adalah pembungkus yang ditulis dengan baik crypt(), dan secara inheren kompatibel dengan crypt()hash -format, bahkan jika mereka menggunakan algoritme hash yang usang dan / atau tidak aman.
  2. Teruskan Kompatibilitas: Memasukkan password_needs_rehash()dan sedikit logika ke dalam alur kerja otentikasi Anda dapat membuat hash Anda tetap mutakhir dengan algoritme saat ini dan masa depan dengan kemungkinan tidak ada perubahan masa depan pada alur kerja. Catatan: Setiap string yang tidak cocok dengan algoritme yang ditentukan akan ditandai karena perlu diulang, termasuk hash yang tidak kompatibel dengan crypt.

Misalnya:

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

Keluaran:

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)

Sebagai catatan terakhir, mengingat Anda hanya dapat mencirikan ulang sandi pengguna saat masuk, Anda harus mempertimbangkan untuk "menghentikan" hash lama yang tidak aman untuk melindungi pengguna Anda. Maksud saya, setelah masa tenggang tertentu Anda menghapus semua hash yang tidak aman [misalnya: MD5 / SHA / jika tidak lemah] yang tidak aman dan meminta pengguna Anda mengandalkan mekanisme penyetelan ulang sandi aplikasi Anda.

Sammitch
sumber
0

Kode lengkap Kata Sandi Kelas:

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

}
Dimitris Maniatis
sumber
0

Saya telah membangun sebuah fungsi yang saya gunakan setiap saat untuk validasi kata sandi dan untuk membuat kata sandi, misalnya untuk menyimpannya dalam database MySQL. Ini menggunakan garam yang dihasilkan secara acak yang jauh lebih aman daripada menggunakan garam statis.

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;

}
Gerrit Fries
sumber
6
Cara terbaik adalah menghilangkan saltparameter, ini akan dibuat secara otomatis oleh fungsi password_hash () , mengikuti praktik terbaik. Alih-alih PASSWORD_BCRYPTsatu dapat digunakan PASSWORD_DEFAULTuntuk menulis kode bukti masa depan.
martinstoeckli
Terima kasih atas saran itu. Aku pasti sudah mengawasinya di dokumentasi. Ini malam yang panjang.
Gerrit Fries
1
Menurut secure.php.net/manual/en/function.password-hash.php "Opsi garam sudah tidak digunakan lagi mulai PHP 7.0.0. Sekarang lebih disukai hanya menggunakan garam yang dihasilkan secara default."
jmng