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