Overview

Packages

  • Auth

Classes

  • Horde_Auth
  • Horde_Auth_Auto
  • Horde_Auth_Base
  • Horde_Auth_Composite
  • Horde_Auth_Customsql
  • Horde_Auth_Cyrsql
  • Horde_Auth_Exception
  • Horde_Auth_Ftp
  • Horde_Auth_Http
  • Horde_Auth_Http_Remote
  • Horde_Auth_Imap
  • Horde_Auth_Ipbasic
  • Horde_Auth_Kolab
  • Horde_Auth_Ldap
  • Horde_Auth_Login
  • Horde_Auth_Msad
  • Horde_Auth_Pam
  • Horde_Auth_Passwd
  • Horde_Auth_Peclsasl
  • Horde_Auth_Radius
  • Horde_Auth_Shibboleth
  • Horde_Auth_Smb
  • Horde_Auth_Smbclient
  • Horde_Auth_Sql
  • Horde_Auth_Translation
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * The Horde_Auth class provides some useful authentication-related utilities
  4:  * and constants for the Auth package.
  5:  *
  6:  * Copyright 1999-2012 Horde LLC (http://www.horde.org/)
  7:  *
  8:  * See the enclosed file COPYING for license information (LGPL). If you did
  9:  * not receive this file, http://www.horde.org/licenses/lgpl21
 10:  *
 11:  * @author   Chuck Hagenbuch <chuck@horde.org>
 12:  * @author   Michael Slusarz <slusarz@horde.org>
 13:  * @category Horde
 14:  * @license  http://www.horde.org/licenses/lgpl21 LGPL-2.1
 15:  * @package  Auth
 16:  */
 17: class Horde_Auth
 18: {
 19:     /**
 20:      * Authentication failure reasons.
 21:      *
 22:      * <pre>
 23:      * REASON_BADLOGIN - Bad username and/or password
 24:      * REASON_FAILED   - Login failed
 25:      * REASON_EXPIRED  - Password has expired
 26:      * REASON_LOGOUT   - Logout due to user request
 27:      * REASON_MESSAGE  - Logout with custom message
 28:      * REASON_SESSION  - Logout due to session expiration
 29:      * REASON_LOCKED   - User is locked
 30:      * </pre>
 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:      * 64 characters that are valid for APRMD5 passwords.
 42:      */
 43:     const APRMD5_VALID = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
 44: 
 45:     /**
 46:      * Characters used when generating a password.
 47:      */
 48:     const VOWELS = 'aeiouy';
 49:     const CONSONANTS = 'bcdfghjklmnpqrstvwxz';
 50:     const NUMBERS = '0123456789';
 51: 
 52:     /**
 53:      * Attempts to return a concrete Horde_Auth_Base instance based on
 54:      * $driver.
 55:      *
 56:      * @param string $driver  Either a driver name, or the full class name to
 57:      *                        use (class must extend Horde_Auth_Base).
 58:      * @param array $params   A hash containing any additional configuration
 59:      *                        or parameters a subclass might need.
 60:      *
 61:      * @return Horde_Auth_Base  The newly created concrete instance.
 62:      * @throws Horde_Auth_Exception
 63:      */
 64:     static public function factory($driver, $params = null)
 65:     {
 66:         /* Base drivers (in Auth/ directory). */
 67:         $class = __CLASS__ . '_' . ucfirst($driver);
 68:         if (@class_exists($class)) {
 69:             return new $class($params);
 70:         }
 71: 
 72:         /* Explicit class name, */
 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:      * Formats a password using the current encryption.
 83:      *
 84:      * @param string $plaintext      The plaintext password to encrypt.
 85:      * @param string $salt           The salt to use to encrypt the password.
 86:      *                               If not present, a new salt will be
 87:      *                               generated.
 88:      * @param string $encryption     The kind of pasword encryption to use.
 89:      *                               Defaults to md5-hex.
 90:      * @param boolean $show_encrypt  Some password systems prepend the kind of
 91:      *                               encryption to the crypted password ({SHA},
 92:      *                               etc). Defaults to false.
 93:      *
 94:      * @return string  The encrypted password.
 95:      */
 96:     static public function getCryptedPassword($plaintext, $salt = '',
 97:                                               $encryption = 'md5-hex',
 98:                                               $show_encrypt = false)
 99:     {
100:         /* Get the salt to use. */
101:         $salt = self::getSalt($encryption, $salt, $plaintext);
102: 
103:         /* Encrypt the password. */
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:      * Returns a salt for the appropriate kind of password encryption.
190:      * Optionally takes a seed and a plaintext password, to extract the seed
191:      * of an existing password, or for encryption types that use the plaintext
192:      * in the generation of the salt.
193:      *
194:      * @param string $encryption  The kind of pasword encryption to use.
195:      *                            Defaults to md5-hex.
196:      * @param string $seed        The seed to get the salt from (probably a
197:      *                            previously generated password). Defaults to
198:      *                            generating a new seed.
199:      * @param string $plaintext   The plaintext password that we're generating
200:      *                            a salt for. Defaults to none.
201:      *
202:      * @return string  The generated or extracted salt.
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:      * Converts to allowed 64 characters for APRMD5 passwords.
269:      *
270:      * @param string $value   The value to convert
271:      * @param integer $count  The number of iterations
272:      *
273:      * @return string  $value converted to the 64 MD5 characters.
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:      * Generates a random, hopefully pronounceable, password. This can be used
291:      * when resetting automatically a user's password.
292:      *
293:      * @return string A random password
294:      */
295:     static public function genRandomPassword()
296:     {
297:         /* Alternate consonant and vowel random chars with two random numbers
298:          * at the end. This should produce a fairly pronounceable password. */
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:      * Checks whether a password matches some expected policy.
310:      *
311:      * @since   Horde_Auth 1.4.0
312:      *
313:      * @param string $password  A password.
314:      * @param array $policy     A configuration with policy rules. Supported
315:      *                          rules:
316:      *
317:      *   - minLength:   Minimum length of the password
318:      *   - maxLength:   Maximum length of the password
319:      *   - maxSpace:    Maximum number of white space characters
320:      *
321:      *     The following are the types of characters required in a
322:      *     password.  Either specific characters, character classes,
323:      *     or both can be required.  Specific types are:
324:      *
325:      *   - minUpper:    Minimum number of uppercase characters
326:      *   - minLower:    Minimum number of lowercase characters
327:      *   - minNumeric:  Minimum number of numeric characters (0-9)
328:      *   - minAlphaNum: Minimum number of alphanumeric characters
329:      *   - minAlpha:    Minimum number of alphabetic characters
330:      *   - minSymbol:   Minimum number of alphabetic characters
331:      *
332:      *     Alternatively (or in addition to), the minimum number of
333:      *     character classes can be configured by setting the
334:      *     following.  The valid range is 0 through 4 character
335:      *     classes may be required for a password. The classes are:
336:      *     'upper', 'lower', 'number', and 'symbol'.  For example: A
337:      *     password of 'p@ssw0rd' satisfies three classes ('number',
338:      *     'lower', and 'symbol'), while 'passw0rd' only satisfies two
339:      *     classes ('lower' and 'symbols').
340:      *
341:      *   - minClasses:  Minimum number (0 through 4) of character
342:      *                  classes.
343:      *
344:      * @throws Horde_Auth_Exception if the password does not match the policy.
345:      */
346:     static public function checkPasswordPolicy($password, array $policy)
347:     {
348:         // Check max/min lengths if specified in the policy.
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:         // Dissect the password in a localized way.
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:         // Check reamaining password policy options.
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:      * Checks whether a password is too similar to a dictionary of strings.
408:      *
409:      * @since   Horde_Auth 1.4.0
410:      *
411:      * @param string $password  A password.
412:      * @param array $dict       A dictionary to check for similarity, for
413:      *                          example the user name or an old password.
414:      * @param float $percent    The maximum allowed similarity in percent.
415:      *
416:      * @throws Horde_Auth_Exception if the password is too similar.
417:      */
418:     static public function checkPasswordSimilarity($password, array $dict,
419:                                                    $max = 80)
420:     {
421:         // Check for pass == dict, simple reverse strings, etc.
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:         // Check for percentages similarity also.  This will catch very simple
430:         // Things like "password" -> "password2" or "xpasssword"...
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: 
API documentation generated by ApiGen