November 30, 2016 —John Koster
In this article we will create an implementation of Illuminate\Contracts\Hashing\Hasher
using PHP's crypt
function and the CRYPT_EXT_DES
hashing function. Like in the previous section, we will examine each method before looking at the full implementation.
make($value, array $options = [])
The make
method is responsible for doing the actual hashing. It also accepts an $options
array. We will allow a salt to be supplied to the options array. If no salt is supplied, we will generate one for the user. We will also accept a rounds option, to control the iterations. To make things easier for users, we will accept an integer for the rounds option and covert it base64 before calculating the hash.
We will need a function to convert an integer to base64.
For the actual hashing, we will make a call to PHP's crypt
function.
public function check($value, $hashedValue, array $options = [])
The check
method is used to check that a known $value
is the same as a $hashedValue
. We will not need any options for this method.
Internally, we will use the make
method and PHP's hash_equals
function.
public function needsRehash($hashedValue, array $options = [])
The needsRehash
method is used to determine if a given hash needs to be updated, or rehashed. We will accept both a salt and a number of rounds as valid options. If either the salt or number of rounds differ from those stored in the $hashedValue
, we will say that the $hashedValue
needs to be rehashed.
The following is the complete implementation of the ExtendedDesHasher
class:
ExtendedDesHasher Option | Description |
---|---|
salt |
Users can supply their own four character salt to the make and needsRehash methods. If no salt is supplied for the make method, one will be generated. |
rounds |
Users can give an integer for the make and needsRehash methods to change the number of iterations the hashing function will perform. The integer will be converted to base64 for users automatically. |
base64rounds |
Users can supply a base64 encoded integer directly to the make method. If present, the base64rounds option takes precedence over the rounds option. |
1<?php 2 3namespace Laravel\Artisan\Hashing; 4 5use RuntimeException; 6use Illuminate\Contracts\Hashing\Hasher as HasherContract; 7use Illuminate\Support\Str; 8 9class ExtendedDesHasher implements HasherContract 10{ 11 12 const FAILED_HASH = '*0'; 13 14 /** 15 * Default number of rounds. 16 * 17 * @var int 18 */ 19 protected $rounds = 5000; 20 21 /** 22 * Hash the given value. 23 * 24 * @param string $value 25 * @param array $options 26 * @return string 27 * 28 * @throws \RuntimeException 29 */ 30 public function make($value, array $options = []) 31 { 32 // If the user supplied a salt, use that. If not we 33 // can generate it using the Str helper methods. 34 $salt = isset($options['salt']) ? $options['salt'] : Str::random(4); 35 $rounds = $this->toBase64( 36 isset($options['rounds']) ? $options['rounds'] : $this->rounds 37 ); 38 39 // If the user supplied a 'base64rounds' option, 40 // let's use that instead. 41 $rounds = isset($options['base64rounds']) ? 42 $options['base64rounds'] : $rounds; 43 44 $hash = crypt($value, '_' . $rounds . $salt); 45 46 if ($hash == self::FAILED_HASH) { 47 // Throw an exception because the hashing failed. 48 throw new RuntimeException('Extended DES hashing failed.'); 49 } 50 51 return $hash; 52 } 53 54 /** 55 * Check the given plain value against a hash. 56 * 57 * @param string $value 58 * @param string $hashedValue 59 * @param array $options 60 * @return bool 61 */ 62 public function check($value, $hashedValue, array $options = []) 63 { 64 // Get the information required to rehash based on the hash. 65 $hashParts = $this->getHashInformation($hashedValue); 66 $userValue = $this->make($value, [ 67 'base64rounds' => $hashParts['rounds'], 68 'salt' => $hashParts['salt'] 69 ]); 70 71 return hash_equals($hashedValue, $userValue); 72 } 73 74 /** 75 * Check if the given hash has been hashed using the given options. 76 * 77 * @param string $hashedValue 78 * @param array $options 79 * @return bool 80 */ 81 public function needsRehash($hashedValue, array $options = []) 82 { 83 if (!isset($options['salt']) && !isset($options['rounds'])) { 84 return false; 85 } 86 87 $hashParts = $this->getHashInformation($hashedValue); 88 89 // If the salts differ, hash needs to be recomputed. 90 if (isset($options['salt']) && $hashParts['salt'] 91 !== $options['salt'] 92 ) { 93 return true; 94 } 95 96 // If the number of rounds differ, the hash 97 // needs to be recalculated. 98 if (isset($options['rounds']) && $hashParts['rounds'] 99 !== $this->toBase64($options['rounds'])100 ) {101 return true;102 }103 104 return false;105 }106 107 /**108 * Returns information about a CRYPT_EXT_DES hash.109 *110 * @param $hashedValue111 * @return array112 */113 private function getHashInformation($hashedValue)114 {115 $hashParts = str_split(mb_substr($hashedValue, 1), 4);116 117 return [118 'rounds' => $hashParts[0],119 'salt' => $hashParts[1]120 ];121 }122 123 /**124 * Converts an integer to base64.125 *126 * @param $integer127 * @return string128 */129 private function toBase64($integer){130 $alphabet_raw = './0123456789abcdefghijklmnopqrstuvwxyz'.131 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';132 133 $alphabet = str_split($alphabet_raw);134 $base = sizeof($alphabet);135 136 while($integer){137 $rem = $integer % $base;138 $integer = (int) ($integer / $base);139 $arr[] = $alphabet[$rem];140 }141 142 $arr = array_reverse($arr);143 $string = implode($arr);144 145 return str_pad($string, 4, '.', STR_PAD_LEFT);146 }147 148 /**149 * Set the default number of rounds.150 *151 * @param int $rounds152 * @return $this153 */154 public function setRounds($rounds)155 {156 $this->rounds = (int)$rounds;157 return $this;158 }159 160}
∎
The following amazing people help support this site and my open source projects ♥️
If you're interesting in supporting my work and want to show up on this list, check out my GitHub Sponsors Profile.