Salting and Hashing

Salting

Salting is putting random characters in a password before hashing. It derives its name because random "grains" of data are "sprinkled" on the password like table salt. The idea is to use these characters to create unique passwords for users despite the fact two end users type the exact same thing at the keyboard. For instance, say we have two users whose password is p@ssword and the two users' salts are 4675636b696e and 4472756d7066, respectively. Their salted passwords would look something like this:

  • password 0: 467563p@ssword6b696e
  • password 1: 447275p@ssword6d7066

The order in which the salt grains are inserted into the password is unimportant, so long as its a consistent pattern for every password. These invisible changes to the users' passwords greatly enhance security because now we will not obtain duplicate hashes for two users typing the same passwords. The hashes will be very different from one another despite being generated from a common root password. This is known as the avalanche effect.

Hashing & Hash Stretching

Hashing is taking a password and turning into a mathematical sequence that CANNOT be reversed. That is, if you have a hash of somebody's password, you have no way to find out what generated the password. e.g., abc123 generates the hash c70b5dd9ebfb6f51d09d4132b7170c9d20750a7852f00680f65658f0310e810056e6763c34c9a00b0e940076f54495c169fc2302cceb312039271c43469507dc when using SHA512.

Hash stretching is a method of slowing down hashing a password. The reason to slow down a hash is to defend against a timing attack, where an attacker can infer the strength of one's password by observing the performance of the hash. Hash stretching works by salting the password and hashing it, and then hashing the hash, and then hashing the hash of the hash, and then hashing the hash of the hash of the hash, …, until a certain number of iterations are reached. The advantage of this strategy is twofold:

  1. slow: by slowing the process down, attackers need to do exponentially more work
  2. normalized timings: stretched hashes, statistically speaking, take the same amount of time and give no information to the attacker

Hash stretching is effective against brute force and lookup attacks.

Hashing and Salting a Password

<?php
$password = "abc123";
$hash = password_hash($password, PASSWORD_ARGON2I, ["time_cost" => 384]);

The Argon algorithm was introduced in PHP 7.2 to the password_hash function. It was the winner of the Password Hashing Competition and was chosen as a winner for its resistance to GPU cracking attacks. Argon has three parameters:

  • Memory Cost: how much memory Argon is constrained to
  • Time Cost: how much to slow down Argon
  • Threads: how many threads to use to calculate the hash

The time cost parameter used at Deep Dive is 384. This was derived from achieving a 0.5 second execution time using a Monte Carlo simulation over 32 iterations. This parameter is application/server specific and should be tuned to fit the use case. The advantage to Argon's use of these parameters is that they can be updated during the life of the application without losing the context of the previous hashes. These parameters are embedded directly in the hash, delimited by the $ character. For instance, $argon2i$v=19$m=1024,t=384,p=2$dE55dm5kRm9DTEYxNFlFUA$nNEMItrDUtwnDhZ41nwVm7ncBLrJzjh5mGIjj8RlzVA breaks down to:

  • argon2i: the algorithm used, as password_hash supports other algorithms
  • v=19: version 19
  • m=1024,t=384,p=2:
    • memory cost: 1024
    • time cost: 384
    • threads: 2
  • dE55dm5kRm9DTEYxNFlFUA: salt, generated by Argon automatically
  • nNEMItrDUtwnDhZ41nwVm7ncBLrJzjh5mGIjj8RlzVA: actual hash

Note the time cost is reflected as 384. This is set in the third parameter of password_hash in the code above.

Verifying a Hash in an Application

<?php
$password = "abc123";
if(password_verify($password, $profile->getHash()) === false) {
	throw(new \InvalidArgumentException("incorrect username/password"));
}

PHP's builtin password_verify function takes care of the verification for us. It returns a bool whether the password matches the hash or not and is compatible with any hash derived from password_hash.

Verifying a Hash in a Mutator Method

<?php
class Profile {
	// ...rest of class omitted for brevity...

	/**
	 * mutator method for profile hash password
	 *
	 * @param string $newProfileHash
	 * @throws \InvalidArgumentException if the hash is not secure
	 * @throws \RangeException if the hash is not 128 characters
	 * @throws \TypeError if profile hash is not a string
	 */
	public function setProfileHash(string $newProfileHash): void {
		//enforce that the hash is properly formatted
		$newProfileHash = trim($newProfileHash);
		if(empty($newProfileHash) === true) {
			throw(new \InvalidArgumentException("profile password hash empty or insecure"));
		}

		//enforce the hash is really an Argon hash
		$profileHashInfo = password_get_info($newProfileHash);
		if($profileHashInfo["algoName"] !== "argon2i") {
			throw(new \InvalidArgumentException("profile hash is not a valid hash"));
		}

		//enforce that the hash is exactly 97 characters.
		if(strlen($newProfileHash) !== 97) {
			throw(new \RangeException("profile hash must be 97 characters"));
		}

		//store the hash
		$this->profileHash = $newProfileHash;
	}
}