I know this kind of question has been asked here more than many times, but under every question there is recommendation to ask these things to someone who knows what they are doing. (so we can learn what we are doing as well)
Also I couldn't find enough satisfying examples with updated php functions..
So here is my hashing class which I am developing for an open-source project. I have 4 steps
The parts I am not sure about are key lengths, should I use another base64 encode in order to prevent raw bytes problem with sha256 and is the resulting hash has right format for direct insert to mysql.
Also how secure the class usage itself.
Here is the class:
<?php
namespace shotwn\lazywork;
/**
* add manual here
* pepper is a static server-side key, generated with hash_hmac sha256 and random keys
*
* cholesterol is a random, 22digit?(need more?) database stored key which has been used as salt with
* hash_hmac sha256
*
* main structure is
* password_hash(hash_hmac(sha256, hash_hmac(sha256, base64_encode(password), pepper),cholesterol))
*
*/
class PasswordKitchen {
private static $password_pepper;
function __construct() {
try {
self::$password_pepper = include "/../.nope/biber.key";
} catch (Exception $e) {
throw new Exception("No pepper key");
}
}
private function season(string $password, string $cholesterol = null) {
//use site-wide password pepper
$password_safe = base64_encode ($password);
if(isset($cholesterol) && $cholesterol != null) {
$password_cholesterol = $cholesterol;
} else {
$password_cholesterol = substr(base64_encode(openssl_random_pseudo_bytes(17)),0,22);; //will be user-based mysql recorded
$password_cholesterol = str_replace("+",".",$password_cholesterol);
}
$password_with_pepper = hash_hmac("sha256",$password_safe,self::$password_pepper);
$password_with_pepper_and_cholesterol = hash_hmac("sha256",$password_with_pepper,$password_cholesterol);
$seasonedPassword = (array) [
"password_w_PaC" => $password_with_pepper_and_cholesterol,
"password_cholesterol" => $password_cholesterol,
];
return $seasonedPassword;
}
public function hash(string $password, $cost = 16) {
$options = [
'cost' => $cost, //change for admin accounts
];
$seasoning = $this->season($password);
$seasoned_password = $seasoning["password_w_PaC"];
$password_cholesterol = $seasoning["password_cholesterol"];
$passwordHash = password_hash($seasoned_password, PASSWORD_DEFAULT);
return (array) [
"hash" => $passwordHash,
"cholesterol" => $password_cholesterol,
];
}
public function validate(string $password, string $cholesterol, string $hash) {
$seasonThePassword = $this->season($password, $cholesterol);
return password_verify($seasonThePassword["password_w_PaC"], $hash);
}
}
This is what password_hash() already does:
So there is no need to take extra steps to safely store your passwords. Especially the generation of the random salt (cholesterol) is already done by the function. The cost factor you pass to your function is never used.
So i would recommend to just use the password_hash() directly:
// Hash a new password for storing in the database.
// The function automatically generates a cryptographically safe salt.
$hashToStoreInDb = password_hash($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($password, $existingHashFromDb);
If you want a higher cost factor you can pass it in the options, be aware that increasing the cost factor by one, will double the calculation time, 16 seems to be an unnecessary high factor.
If you want to include a server side secret, there is a better way as to add it as a pepper. Instead encrypt the resulting hash. Further explanations you can find at the end of my tutorial about safely storing passwords.