Laravel: Implementing a CRYPT_EXT_DES Hasher

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 $hashedValue
111 * @return array
112 */
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 $integer
127 * @return string
128 */
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 $rounds
152 * @return $this
153 */
154 public function setRounds($rounds)
155 {
156 $this->rounds = (int)$rounds;
157 return $this;
158 }
159 
160}

Some absolutely amazing
people

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.