d7afecd6eee99cfdf5f2c987cb584cad8e8080c9
[yaffs-website] / web / core / lib / Drupal / Core / Password / PhpassHashedPassword.php
1 <?php
2
3 namespace Drupal\Core\Password;
4
5 use Drupal\Component\Utility\Crypt;
6
7 /**
8  * Secure password hashing functions based on the Portable PHP password
9  * hashing framework.
10  *
11  * @see http://www.openwall.com/phpass/
12  */
13 class PhpassHashedPassword implements PasswordInterface {
14   /**
15    * The minimum allowed log2 number of iterations for password stretching.
16    */
17   const MIN_HASH_COUNT = 7;
18
19   /**
20    * The maximum allowed log2 number of iterations for password stretching.
21    */
22   const MAX_HASH_COUNT = 30;
23
24   /**
25    * The expected (and maximum) number of characters in a hashed password.
26    */
27   const HASH_LENGTH = 55;
28
29   /**
30    * Returns a string for mapping an int to the corresponding base 64 character.
31    *
32    * @var string
33    */
34   public static $ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
35
36   /**
37    * Specifies the number of times the hashing function will be applied when
38    * generating new password hashes. The number of times is calculated by
39    * raising 2 to the power of the given value.
40    */
41   protected $countLog2;
42
43   /**
44    * Constructs a new password hashing instance.
45    *
46    * @param int $countLog2
47    *   Password stretching iteration count. Specifies the number of times the
48    *   hashing function will be applied when generating new password hashes.
49    *   The number of times is calculated by raising 2 to the power of the given
50    *   value.
51    */
52   public function __construct($countLog2) {
53     // Ensure that $countLog2 is within set bounds.
54     $this->countLog2 = $this->enforceLog2Boundaries($countLog2);
55   }
56
57   /**
58    * Encodes bytes into printable base 64 using the *nix standard from crypt().
59    *
60    * @param string $input
61    *   The string containing bytes to encode.
62    * @param int $count
63    *   The number of characters (bytes) to encode.
64    *
65    * @return string
66    *   Encoded string.
67    */
68   protected function base64Encode($input, $count) {
69     $output = '';
70     $i = 0;
71     do {
72       $value = ord($input[$i++]);
73       $output .= static::$ITOA64[$value & 0x3f];
74       if ($i < $count) {
75         $value |= ord($input[$i]) << 8;
76       }
77       $output .= static::$ITOA64[($value >> 6) & 0x3f];
78       if ($i++ >= $count) {
79         break;
80       }
81       if ($i < $count) {
82         $value |= ord($input[$i]) << 16;
83       }
84       $output .= static::$ITOA64[($value >> 12) & 0x3f];
85       if ($i++ >= $count) {
86         break;
87       }
88       $output .= static::$ITOA64[($value >> 18) & 0x3f];
89     } while ($i < $count);
90
91     return $output;
92   }
93
94   /**
95    * Generates a random base 64-encoded salt prefixed with hash settings.
96    *
97    * Proper use of salts may defeat a number of attacks, including:
98    *  - The ability to try candidate passwords against multiple hashes at once.
99    *  - The ability to use pre-hashed lists of candidate passwords.
100    *  - The ability to determine whether two users have the same (or different)
101    *    password without actually having to guess one of the passwords.
102    *
103    * @return string
104    *   A 12 character string containing the iteration count and a random salt.
105    */
106   protected function generateSalt() {
107     $output = '$S$';
108     // We encode the final log2 iteration count in base 64.
109     $output .= static::$ITOA64[$this->countLog2];
110     // 6 bytes is the standard salt for a portable phpass hash.
111     $output .= $this->base64Encode(Crypt::randomBytes(6), 6);
112     return $output;
113   }
114
115   /**
116    * Ensures that $count_log2 is within set bounds.
117    *
118    * @param int $count_log2
119    *   Integer that determines the number of iterations used in the hashing
120    *   process. A larger value is more secure, but takes more time to complete.
121    *
122    * @return int
123    *   Integer within set bounds that is closest to $count_log2.
124    */
125   protected function enforceLog2Boundaries($count_log2) {
126     if ($count_log2 < static::MIN_HASH_COUNT) {
127       return static::MIN_HASH_COUNT;
128     }
129     elseif ($count_log2 > static::MAX_HASH_COUNT) {
130       return static::MAX_HASH_COUNT;
131     }
132
133     return (int) $count_log2;
134   }
135
136   /**
137    * Hash a password using a secure stretched hash.
138    *
139    * By using a salt and repeated hashing the password is "stretched". Its
140    * security is increased because it becomes much more computationally costly
141    * for an attacker to try to break the hash by brute-force computation of the
142    * hashes of a large number of plain-text words or strings to find a match.
143    *
144    * @param string $algo
145    *   The string name of a hashing algorithm usable by hash(), like 'sha256'.
146    * @param string $password
147    *   Plain-text password up to 512 bytes (128 to 512 UTF-8 characters) to
148    *   hash.
149    * @param string $setting
150    *   An existing hash or the output of $this->generateSalt(). Must be at least
151    *   12 characters (the settings and salt).
152    *
153    * @return string
154    *   A string containing the hashed password (and salt) or FALSE on failure.
155    *   The return string will be truncated at HASH_LENGTH characters max.
156    */
157   protected function crypt($algo, $password, $setting) {
158     // Prevent DoS attacks by refusing to hash large passwords.
159     if (strlen($password) > PasswordInterface::PASSWORD_MAX_LENGTH) {
160       return FALSE;
161     }
162
163     // The first 12 characters of an existing hash are its setting string.
164     $setting = substr($setting, 0, 12);
165
166     if ($setting[0] != '$' || $setting[2] != '$') {
167       return FALSE;
168     }
169     $count_log2 = $this->getCountLog2($setting);
170     // Stored hashes may have been crypted with any iteration count. However we
171     // do not allow applying the algorithm for unreasonable low and high values
172     // respectively.
173     if ($count_log2 != $this->enforceLog2Boundaries($count_log2)) {
174       return FALSE;
175     }
176     $salt = substr($setting, 4, 8);
177     // Hashes must have an 8 character salt.
178     if (strlen($salt) != 8) {
179       return FALSE;
180     }
181
182     // Convert the base 2 logarithm into an integer.
183     $count = 1 << $count_log2;
184
185     // We rely on the hash() function being available in PHP 5.2+.
186     $hash = hash($algo, $salt . $password, TRUE);
187     do {
188       $hash = hash($algo, $hash . $password, TRUE);
189     } while (--$count);
190
191     $len = strlen($hash);
192     $output = $setting . $this->base64Encode($hash, $len);
193     // $this->base64Encode() of a 16 byte MD5 will always be 22 characters.
194     // $this->base64Encode() of a 64 byte sha512 will always be 86 characters.
195     $expected = 12 + ceil((8 * $len) / 6);
196     return (strlen($output) == $expected) ? substr($output, 0, static::HASH_LENGTH) : FALSE;
197   }
198
199   /**
200    * Parses the log2 iteration count from a stored hash or setting string.
201    *
202    * @param string $setting
203    *   An existing hash or the output of $this->generateSalt(). Must be at least
204    *   12 characters (the settings and salt).
205    *
206    * @return int
207    *   The log2 iteration count.
208    */
209   public function getCountLog2($setting) {
210     return strpos(static::$ITOA64, $setting[3]);
211   }
212
213   /**
214    * {@inheritdoc}
215    */
216   public function hash($password) {
217     return $this->crypt('sha512', $password, $this->generateSalt());
218   }
219
220   /**
221    * {@inheritdoc}
222    */
223   public function check($password, $hash) {
224     if (substr($hash, 0, 2) == 'U$') {
225       // This may be an updated password from user_update_7000(). Such hashes
226       // have 'U' added as the first character and need an extra md5() (see the
227       // Drupal 7 documentation).
228       $stored_hash = substr($hash, 1);
229       $password = md5($password);
230     }
231     else {
232       $stored_hash = $hash;
233     }
234
235     $type = substr($stored_hash, 0, 3);
236     switch ($type) {
237       case '$S$':
238         // A normal Drupal 7 password using sha512.
239         $computed_hash = $this->crypt('sha512', $password, $stored_hash);
240         break;
241       case '$H$':
242         // phpBB3 uses "$H$" for the same thing as "$P$".
243       case '$P$':
244         // A phpass password generated using md5.  This is an
245         // imported password or from an earlier Drupal version.
246         $computed_hash = $this->crypt('md5', $password, $stored_hash);
247         break;
248       default:
249         return FALSE;
250     }
251
252     // Compare using hashEquals() instead of === to mitigate timing attacks.
253     return $computed_hash && Crypt::hashEquals($stored_hash, $computed_hash);
254   }
255
256   /**
257    * {@inheritdoc}
258    */
259   public function needsRehash($hash) {
260     // Check whether this was an updated password.
261     if ((substr($hash, 0, 3) != '$S$') || (strlen($hash) != static::HASH_LENGTH)) {
262       return TRUE;
263     }
264     // Ensure that $count_log2 is within set bounds.
265     $count_log2 = $this->enforceLog2Boundaries($this->countLog2);
266     // Check whether the iteration count used differs from the standard number.
267     return ($this->getCountLog2($hash) !== $count_log2);
268   }
269
270 }