blob: 7f0b41289da25dcc5b2d090691821bf7996cb1bf [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace LdapRecord\Models\Attributes;
4
5use InvalidArgumentException;
6use LdapRecord\LdapRecordException;
7use ReflectionMethod;
8
9class Password
10{
11 const CRYPT_SALT_TYPE_MD5 = 1;
12 const CRYPT_SALT_TYPE_SHA256 = 5;
13 const CRYPT_SALT_TYPE_SHA512 = 6;
14
15 /**
16 * Make an encoded password for transmission over LDAP.
17 *
18 * @param string $password
19 *
20 * @return string
21 */
22 public static function encode($password)
23 {
24 return iconv('UTF-8', 'UTF-16LE', '"'.$password.'"');
25 }
26
27 /**
28 * Make a salted md5 password.
29 *
30 * @param string $password
31 * @param null|string $salt
32 *
33 * @return string
34 */
35 public static function smd5($password, $salt = null)
36 {
37 return '{SMD5}'.static::makeHash($password, 'md5', null, $salt ?? random_bytes(4));
38 }
39
40 /**
41 * Make a salted SHA password.
42 *
43 * @param string $password
44 * @param null|string $salt
45 *
46 * @return string
47 */
48 public static function ssha($password, $salt = null)
49 {
50 return '{SSHA}'.static::makeHash($password, 'sha1', null, $salt ?? random_bytes(4));
51 }
52
53 /**
54 * Make a salted SSHA256 password.
55 *
56 * @param string $password
57 * @param null|string $salt
58 *
59 * @return string
60 */
61 public static function ssha256($password, $salt = null)
62 {
63 return '{SSHA256}'.static::makeHash($password, 'hash', 'sha256', $salt ?? random_bytes(4));
64 }
65
66 /**
67 * Make a salted SSHA384 password.
68 *
69 * @param string $password
70 * @param null|string $salt
71 *
72 * @return string
73 */
74 public static function ssha384($password, $salt = null)
75 {
76 return '{SSHA384}'.static::makeHash($password, 'hash', 'sha384', $salt ?? random_bytes(4));
77 }
78
79 /**
80 * Make a salted SSHA512 password.
81 *
82 * @param string $password
83 * @param null|string $salt
84 *
85 * @return string
86 */
87 public static function ssha512($password, $salt = null)
88 {
89 return '{SSHA512}'.static::makeHash($password, 'hash', 'sha512', $salt ?? random_bytes(4));
90 }
91
92 /**
93 * Make a non-salted SHA password.
94 *
95 * @param string $password
96 *
97 * @return string
98 */
99 public static function sha($password)
100 {
101 return '{SHA}'.static::makeHash($password, 'sha1');
102 }
103
104 /**
105 * Make a non-salted SHA256 password.
106 *
107 * @param string $password
108 *
109 * @return string
110 */
111 public static function sha256($password)
112 {
113 return '{SHA256}'.static::makeHash($password, 'hash', 'sha256');
114 }
115
116 /**
117 * Make a non-salted SHA384 password.
118 *
119 * @param string $password
120 *
121 * @return string
122 */
123 public static function sha384($password)
124 {
125 return '{SHA384}'.static::makeHash($password, 'hash', 'sha384');
126 }
127
128 /**
129 * Make a non-salted SHA512 password.
130 *
131 * @param string $password
132 *
133 * @return string
134 */
135 public static function sha512($password)
136 {
137 return '{SHA512}'.static::makeHash($password, 'hash', 'sha512');
138 }
139
140 /**
141 * Make a non-salted md5 password.
142 *
143 * @param string $password
144 *
145 * @return string
146 */
147 public static function md5($password)
148 {
149 return '{MD5}'.static::makeHash($password, 'md5');
150 }
151
152 /**
153 * Crypt password with an MD5 salt.
154 *
155 * @param string $password
156 * @param string $salt
157 *
158 * @return string
159 */
160 public static function md5Crypt($password, $salt = null)
161 {
162 return '{CRYPT}'.static::makeCrypt($password, static::CRYPT_SALT_TYPE_MD5, $salt);
163 }
164
165 /**
166 * Crypt password with a SHA256 salt.
167 *
168 * @param string $password
169 * @param string $salt
170 *
171 * @return string
172 */
173 public static function sha256Crypt($password, $salt = null)
174 {
175 return '{CRYPT}'.static::makeCrypt($password, static::CRYPT_SALT_TYPE_SHA256, $salt);
176 }
177
178 /**
179 * Crypt a password with a SHA512 salt.
180 *
181 * @param string $password
182 * @param string $salt
183 *
184 * @return string
185 */
186 public static function sha512Crypt($password, $salt = null)
187 {
188 return '{CRYPT}'.static::makeCrypt($password, static::CRYPT_SALT_TYPE_SHA512, $salt);
189 }
190
191 /**
192 * Make a new password hash.
193 *
194 * @param string $password The password to make a hash of.
195 * @param string $method The hash function to use.
196 * @param string|null $algo The algorithm to use for hashing.
197 * @param string|null $salt The salt to append onto the hash.
198 *
199 * @return string
200 */
201 protected static function makeHash($password, $method, $algo = null, $salt = null)
202 {
203 $params = $algo ? [$algo, $password.$salt] : [$password.$salt];
204
205 return base64_encode(pack('H*', call_user_func($method, ...$params)).$salt);
206 }
207
208 /**
209 * Make a hashed password.
210 *
211 * @param string $password
212 * @param int $type
213 * @param null|string $salt
214 *
215 * @return string
216 */
217 protected static function makeCrypt($password, $type, $salt = null)
218 {
219 return crypt($password, $salt ?? static::makeCryptSalt($type));
220 }
221
222 /**
223 * Make a salt for the crypt() method using the given type.
224 *
225 * @param int $type
226 *
227 * @return string
228 */
229 protected static function makeCryptSalt($type)
230 {
231 [$prefix, $length] = static::makeCryptPrefixAndLength($type);
232
233 $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
234
235 while (strlen($prefix) < $length) {
236 $prefix .= substr($chars, random_int(0, strlen($chars) - 1), 1);
237 }
238
239 return $prefix;
240 }
241
242 /**
243 * Determine the crypt prefix and length.
244 *
245 * @param int $type
246 *
247 * @throws InvalidArgumentException
248 *
249 * @return array
250 */
251 protected static function makeCryptPrefixAndLength($type)
252 {
253 switch ($type) {
254 case static::CRYPT_SALT_TYPE_MD5:
255 return ['$1$', 12];
256 case static::CRYPT_SALT_TYPE_SHA256:
257 return ['$5$', 16];
258 case static::CRYPT_SALT_TYPE_SHA512:
259 return ['$6$', 16];
260 default:
261 throw new InvalidArgumentException("Invalid crypt type [$type].");
262 }
263 }
264
265 /**
266 * Attempt to retrieve the hash method used for the password.
267 *
268 * @param string $password
269 *
270 * @return string|void
271 */
272 public static function getHashMethod($password)
273 {
274 if (! preg_match('/^\{(\w+)\}/', $password, $matches)) {
275 return;
276 }
277
278 return $matches[1];
279 }
280
281 /**
282 * Attempt to retrieve the hash method and algorithm used for the password.
283 *
284 * @param string $password
285 *
286 * @return array|void
287 */
288 public static function getHashMethodAndAlgo($password)
289 {
290 if (! preg_match('/^\{(\w+)\}\$([0-9a-z]{1})\$/', $password, $matches)) {
291 return;
292 }
293
294 return [$matches[1], $matches[2]];
295 }
296
297 /**
298 * Attempt to retrieve a salt from the encrypted password.
299 *
300 * @throws LdapRecordException
301 *
302 * @return string
303 */
304 public static function getSalt($encryptedPassword)
305 {
306 // crypt() methods.
307 if (preg_match('/^\{(\w+)\}(\$.*\$).*$/', $encryptedPassword, $matches)) {
308 return $matches[2];
309 }
310
311 // All other methods.
312 if (preg_match('/{([^}]+)}(.*)/', $encryptedPassword, $matches)) {
313 return substr(base64_decode($matches[2]), -4);
314 }
315
316 throw new LdapRecordException('Could not extract salt from encrypted password.');
317 }
318
319 /**
320 * Determine if the hash method requires a salt to be given.
321 *
322 * @param string $method
323 *
324 * @throws \ReflectionException
325 *
326 * @return bool
327 */
328 public static function hashMethodRequiresSalt($method): bool
329 {
330 $parameters = (new ReflectionMethod(static::class, $method))->getParameters();
331
332 foreach ($parameters as $parameter) {
333 if ($parameter->name === 'salt') {
334 return true;
335 }
336 }
337
338 return false;
339 }
340}