1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:
17: class Horde_Auth
18: {
19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31:
32: const REASON_BADLOGIN = 1;
33: const REASON_FAILED = 2;
34: const REASON_EXPIRED = 3;
35: const REASON_LOGOUT = 4;
36: const REASON_MESSAGE = 5;
37: const REASON_SESSION = 6;
38: const REASON_LOCKED = 7;
39:
40: 41: 42:
43: const APRMD5_VALID = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
44:
45: 46: 47:
48: const VOWELS = 'aeiouy';
49: const CONSONANTS = 'bcdfghjklmnpqrstvwxz';
50: const NUMBERS = '0123456789';
51:
52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63:
64: static public function factory($driver, $params = null)
65: {
66:
67: $class = __CLASS__ . '_' . ucfirst($driver);
68: if (@class_exists($class)) {
69: return new $class($params);
70: }
71:
72:
73: $class = $driver;
74: if (@class_exists($class)) {
75: return new $class($params);
76: }
77:
78: throw new Horde_Auth_Exception(__CLASS__ . ': Class definition of ' . $driver . ' not found.');
79: }
80:
81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95:
96: static public function getCryptedPassword($plaintext, $salt = '',
97: $encryption = 'md5-hex',
98: $show_encrypt = false)
99: {
100:
101: $salt = self::getSalt($encryption, $salt, $plaintext);
102:
103:
104: switch ($encryption) {
105: case 'plain':
106: return $plaintext;
107:
108: case 'msad':
109: return Horde_String::convertCharset('"' . $plaintext . '"', 'ISO-8859-1', 'UTF-16LE');
110:
111: case 'sha':
112: case 'sha1':
113: $encrypted = base64_encode(pack('H*', hash('sha1', $plaintext)));
114: return $show_encrypt ? '{SHA}' . $encrypted : $encrypted;
115:
116: case 'crypt':
117: case 'crypt-des':
118: case 'crypt-md5':
119: case 'crypt-sha256':
120: case 'crypt-sha512':
121: case 'crypt-blowfish':
122: return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt);
123:
124: case 'md5-base64':
125: $encrypted = base64_encode(pack('H*', hash('md5', $plaintext)));
126: return $show_encrypt ? '{MD5}' . $encrypted : $encrypted;
127:
128: case 'ssha':
129: $encrypted = base64_encode(pack('H*', hash('sha1', $plaintext . $salt)) . $salt);
130: return $show_encrypt ? '{SSHA}' . $encrypted : $encrypted;
131:
132: case 'sha256':
133: case 'ssha256':
134: $encrypted = base64_encode(pack('H*', hash('sha256', $plaintext . $salt)) . $salt);
135: return $show_encrypt ? '{SSHA256}' . $encrypted : $encrypted;
136:
137: case 'smd5':
138: $encrypted = base64_encode(pack('H*', hash('md5', $plaintext . $salt)) . $salt);
139: return $show_encrypt ? '{SMD5}' . $encrypted : $encrypted;
140:
141: case 'aprmd5':
142: $length = strlen($plaintext);
143: $context = $plaintext . '$apr1$' . $salt;
144: $binary = pack('H*', hash('md5', $plaintext . $salt . $plaintext));
145:
146: for ($i = $length; $i > 0; $i -= 16) {
147: $context .= substr($binary, 0, ($i > 16 ? 16 : $i));
148: }
149: for ($i = $length; $i > 0; $i >>= 1) {
150: $context .= ($i & 1) ? chr(0) : $plaintext[0];
151: }
152:
153: $binary = pack('H*', hash('md5', $context));
154:
155: for ($i = 0; $i < 1000; ++$i) {
156: $new = ($i & 1) ? $plaintext : substr($binary, 0, 16);
157: if ($i % 3) {
158: $new .= $salt;
159: }
160: if ($i % 7) {
161: $new .= $plaintext;
162: }
163: $new .= ($i & 1) ? substr($binary, 0, 16) : $plaintext;
164: $binary = pack('H*', hash('md5', $new));
165: }
166:
167: $p = array();
168: for ($i = 0; $i < 5; $i++) {
169: $k = $i + 6;
170: $j = $i + 12;
171: if ($j == 16) {
172: $j = 5;
173: }
174: $p[] = self::_toAPRMD5((ord($binary[$i]) << 16) |
175: (ord($binary[$k]) << 8) |
176: (ord($binary[$j])),
177: 5);
178: }
179:
180: return '$apr1$' . $salt . '$' . implode('', $p) . self::_toAPRMD5(ord($binary[11]), 3);
181:
182: case 'md5-hex':
183: default:
184: return ($show_encrypt) ? '{MD5}' . hash('md5', $plaintext) : hash('md5', $plaintext);
185: }
186: }
187:
188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203:
204: static public function getSalt($encryption = 'md5-hex', $seed = '',
205: $plaintext = '')
206: {
207: switch ($encryption) {
208: case 'crypt':
209: case 'crypt-des':
210: return $seed
211: ? substr(preg_replace('|^{crypt}|i', '', $seed), 0, 2)
212: : substr(base64_encode(hash('md5', mt_rand(), true)), 0, 2);
213:
214: case 'crypt-md5':
215: return $seed
216: ? substr(preg_replace('|^{crypt}|i', '', $seed), 0, 12)
217: : '$1$' . base64_encode(hash('md5', sprintf('%08X%08X', mt_rand(), mt_rand()), true)) . '$';
218:
219: case 'crypt-blowfish':
220: return $seed
221: ? substr(preg_replace('|^{crypt}|i', '', $seed), 0, 16)
222: : '$2$' . base64_encode(hash('md5', sprintf('%08X%08X%08X', mt_rand(), mt_rand(), mt_rand()), true)) . '$';
223:
224: case 'crypt-sha256':
225: return $seed
226: ? substr(preg_replace('|^{crypt}|i', '', $seed), 0, strrpos($seed, '$'))
227: : '$5$' . base64_encode(hash('md5', sprintf('%08X%08X%08X', mt_rand(), mt_rand(), mt_rand()), true)) . '$';
228:
229: case 'crypt-sha512':
230: return $seed
231: ? substr(preg_replace('|^{crypt}|i', '', $seed), 0, strrpos($seed, '$'))
232: : '$6$' . base64_encode(hash('md5', sprintf('%08X%08X%08X', mt_rand(), mt_rand(), mt_rand()), true)) . '$';
233:
234: case 'ssha':
235: return $seed
236: ? substr(base64_decode(preg_replace('|^{SSHA}|i', '', $seed)), 20)
237: : substr(pack('H*', hash('sha1', substr(pack('h*', hash('md5', mt_rand())), 0, 8) . $plaintext)), 0, 4);
238:
239: case 'sha256':
240: case 'ssha256':
241: return $seed
242: ? substr(base64_decode(preg_replace('|^{SSHA256}|i', '', $seed)), 20)
243: : substr(pack('H*', hash('sha256', substr(pack('h*', hash('md5', mt_rand())), 0, 8) . $plaintext)), 0, 4);
244:
245: case 'smd5':
246: return $seed
247: ? substr(base64_decode(preg_replace('|^{SMD5}|i', '', $seed)), 16)
248: : substr(pack('H*', hash('md5', substr(pack('h*', hash('md5', mt_rand())), 0, 8) . $plaintext)), 0, 4);
249:
250: case 'aprmd5':
251: if ($seed) {
252: return substr(preg_replace('/^\$apr1\$(.{8}).*/', '\\1', $seed), 0, 8);
253: } else {
254: $salt = '';
255: $valid = self::APRMD5_VALID;
256: for ($i = 0; $i < 8; ++$i) {
257: $salt .= $valid[mt_rand(0, 63)];
258: }
259: return $salt;
260: }
261:
262: default:
263: return '';
264: }
265: }
266:
267: 268: 269: 270: 271: 272: 273: 274:
275: static protected function _toAPRMD5($value, $count)
276: {
277: $aprmd5 = '';
278: $count = abs($count);
279: $valid = self::APRMD5_VALID;
280:
281: while (--$count) {
282: $aprmd5 .= $valid[$value & 0x3f];
283: $value >>= 6;
284: }
285:
286: return $aprmd5;
287: }
288:
289: 290: 291: 292: 293: 294:
295: static public function genRandomPassword()
296: {
297: 298:
299: return substr(self::CONSONANTS, mt_rand(0, strlen(self::CONSONANTS) - 1), 1) .
300: substr(self::VOWELS, mt_rand(0, strlen(self::VOWELS) - 1), 1) .
301: substr(self::CONSONANTS, mt_rand(0, strlen(self::CONSONANTS) - 1), 1) .
302: substr(self::VOWELS, mt_rand(0, strlen(self::VOWELS) - 1), 1) .
303: substr(self::CONSONANTS, mt_rand(0, strlen(self::CONSONANTS) - 1), 1) .
304: substr(self::NUMBERS, mt_rand(0, strlen(self::NUMBERS) - 1), 1) .
305: substr(self::NUMBERS, mt_rand(0, strlen(self::NUMBERS) - 1), 1);
306: }
307:
308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345:
346: static public function checkPasswordPolicy($password, array $policy)
347: {
348:
349: if (isset($policy['minLength']) &&
350: strlen($password) < $policy['minLength']) {
351: throw new Horde_Auth_Exception(sprintf(Horde_Auth_Translation::t("The password must be at least %d characters long!"), $policy['minLength']));
352: }
353: if (isset($policy['maxLength']) &&
354: strlen($password) > $policy['maxLength']) {
355: throw new Horde_Auth_Exception(sprintf(Horde_Auth_Translation::t("The password is too long; passwords may not be more than %d characters long!"), $policy['maxLength']));
356: }
357:
358:
359: $classes = array();
360: $alpha = $alnum = $num = $upper = $lower = $space = $symbol = 0;
361: for ($i = 0; $i < strlen($password); $i++) {
362: $char = substr($password, $i, 1);
363: if (ctype_lower($char)) {
364: $lower++; $alpha++; $alnum++; $classes['lower'] = 1;
365: } elseif (ctype_upper($char)) {
366: $upper++; $alpha++; $alnum++; $classes['upper'] = 1;
367: } elseif (ctype_digit($char)) {
368: $num++; $alnum++; $classes['number'] = 1;
369: } elseif (ctype_punct($char)) {
370: $symbol++; $classes['symbol'] = 1;
371: } elseif (ctype_space($char)) {
372: $space++; $classes['symbol'] = 1;
373: }
374: }
375:
376:
377: if (isset($policy['minUpper']) && $policy['minUpper'] > $upper) {
378: throw new Horde_Auth_Exception(sprintf(Horde_Auth_Translation::ngettext("The password must contain at least %d uppercase character.", "The password must contain at least %d uppercase characters.", $policy['minUpper']), $policy['minUpper']));
379: }
380: if (isset($policy['minLower']) && $policy['minLower'] > $lower) {
381: throw new Horde_Auth_Exception(sprintf(Horde_Auth_Translation::ngettext("The password must contain at least %d lowercase character.", "The password must contain at least %d lowercase characters.", $policy['minLower']), $policy['minLower']));
382: }
383: if (isset($policy['minNumeric']) && $policy['minNumeric'] > $num) {
384: throw new Horde_Auth_Exception(sprintf(Horde_Auth_Translation::ngettext("The password must contain at least %d numeric character.", "The password must contain at least %d numeric characters.", $policy['minNumeric']), $policy['minNumeric']));
385: }
386: if (isset($policy['minAlpha']) && $policy['minAlpha'] > $alpha) {
387: throw new Horde_Auth_Exception(sprintf(Horde_Auth_Translation::ngettext("The password must contain at least %d alphabetic character.", "The password must contain at least %d alphabetic characters.", $policy['minAlpha']), $policy['minAlpha']));
388: }
389: if (isset($policy['minAlphaNum']) && $policy['minAlphaNum'] > $alnum) {
390: throw new Horde_Auth_Exception(sprintf(Horde_Auth_Translation::ngettext("The password must contain at least %d alphanumeric character.", "The password must contain at least %d alphanumeric characters.", $policy['minAlphaNum']), $policy['minAlphaNum']));
391: }
392: if (isset($policy['minClasses']) && $policy['minClasses'] > array_sum($classes)) {
393: throw new Horde_Auth_Exception(sprintf(Horde_Auth_Translation::t("The password must contain at least %d different types of characters. The types are: lower, upper, numeric, and symbols."), $policy['minClasses']));
394: }
395: if (isset($policy['maxSpace']) && $policy['maxSpace'] < $space) {
396: if ($policy['maxSpace'] > 0) {
397: throw new Horde_Auth_Exception(sprintf(Horde_Auth_Translation::t("The password must contain less than %d whitespace characters."), $policy['maxSpace'] + 1));
398: }
399: throw new Horde_Auth_Exception(Horde_Auth_Translation::t("The password must not contain whitespace characters."));
400: }
401: if (isset($policy['minSymbol']) && $policy['minSymbol'] > $symbol) {
402: throw new Horde_Auth_Exception(sprintf(Horde_Auth_Translation::ngettext("The password must contain at least %d symbol character.", "The password must contain at least %d symbol characters.", $policy['minSymbol']), $policy['minSymbol']));
403: }
404: }
405:
406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417:
418: static public function checkPasswordSimilarity($password, array $dict,
419: $max = 80)
420: {
421:
422: foreach ($dict as $test) {
423: if ((strcasecmp($password, $test) == 0) ||
424: (strcasecmp($password, strrev($test)) == 0)) {
425: throw new Horde_Auth_Exception(Horde_Auth_Translation::t("The password is too simple to guess."));
426: }
427: }
428:
429:
430:
431: foreach ($dict as $test) {
432: similar_text($password, $test, $percent);
433: if ($percent > $max) {
434: throw new Horde_Auth_Exception(Horde_Auth_Translation::t("The password is too simple to guess."));
435: }
436: }
437: }
438: }
439: