1 <?php
2 /**
3 * Allows pluggable password encryption.
4 * By default, this might be PHP's integrated sha1()
5 * function, but could also be more sophisticated to facilitate
6 * password migrations from other systems.
7 * Use {@link register()} to add new implementations.
8 *
9 * Used in {@link Security::encrypt_password()}.
10 *
11 * @package sapphire
12 * @subpackage security
13 */
14 abstract class PasswordEncryptor {
15
16 /**
17 * @var array
18 */
19 protected static $encryptors = array();
20
21 /**
22 * @return Array Map of encryptor code to the used class.
23 */
24 static function get_encryptors() {
25 return self::$encryptors;
26 }
27
28 /**
29 * Add a new encryptor implementation.
30 *
31 * Note: Due to portability concerns, its not advisable to
32 * override an existing $code mapping with different behaviour.
33 *
34 * @param String $code This value will be stored stored in the
35 * {@link Member->PasswordEncryption} property.
36 * @param String $class Classname of a {@link PasswordEncryptor} subclass
37 */
38 static function register($code, $class) {
39 self::$encryptors[$code] = $class;
40 }
41
42 /**
43 * @param String $code Unique lookup.
44 */
45 static function unregister($code) {
46 if(isset(self::$encryptors[$code])) unset(self::$encryptors[$code]);
47 }
48
49 /**
50 * @param String $algorithm
51 * @return PasswordEncryptor|Boolean Returns FALSE if class was not found
52 */
53 static function create_for_algorithm($algorithm) {
54 if(!isset(self::$encryptors[$algorithm])) {
55 throw new PasswordEncryptor_NotFoundException(
56 sprintf('No implementation found for "%s"', $algorithm)
57 );
58 }
59
60 $classWithArgs = self::$encryptors[$algorithm];
61 $class = (($p = strpos($classWithArgs, '(')) !== false) ? substr($classWithArgs, 0, $p) : $classWithArgs;
62 if(!class_exists($class)) {
63 throw new PasswordEncryptor_NotFoundException(
64 sprintf('No class found for "%s"', $class)
65 );
66 }
67
68 return eval("return new $classWithArgs;");
69 }
70
71 /**
72 * Return a string value stored in the {@link Member->Password} property.
73 * The password should be hashed with {@link salt()} if applicable.
74 *
75 * @param String $password Cleartext password to be hashed
76 * @param String $salt (Optional)
77 * @param Member $member (Optional)
78 * @return String Maximum of 512 characters.
79 */
80 abstract function encrypt($password, $salt = null, $member = null);
81
82 /**
83 * Return a string value stored in the {@link Member->Salt} property.
84 * By default uses sha1() and mt_rand();
85 *
86 * Note: Only used when {@link Security::$useSalt} is TRUE.
87 *
88 * @param String $password Cleartext password
89 * @param Member $member (Optional)
90 * @return String Maximum of 50 characters
91 */
92 function salt($password, $member = null) {
93 return substr(sha1(mt_rand()) . time(), 0, 50);
94 }
95
96 /**
97 * This usually just returns a strict string comparison,
98 * but is necessary for {@link PasswordEncryptor_LegacyPHPHash}.
99 *
100 * @param String $hash1
101 * @param String $hash2
102 * @return boolean
103 */
104 function compare($hash1, $hash2) {
105 return ($hash1 === $hash2);
106 }
107 }
108
109 /**
110 * This is the default class used for built-in hash types in PHP.
111 * Please note that the implemented algorithms depend on the PHP
112 * distribution and architecture.
113 *
114 * @package sapphire
115 * @subpackage security
116 */
117 class PasswordEncryptor_PHPHash extends PasswordEncryptor {
118
119 protected $algorithm = 'sha1';
120
121 /**
122 * @param String $algorithm A PHP built-in hashing algorithm as defined by hash_algos()
123 */
124 function __construct($algorithm) {
125 if(!in_array($algorithm, hash_algos())) {
126 throw new Exception(
127 sprintf('Hash algorithm "%s" not found in hash_algos()', $algorithm)
128 );
129 }
130
131 $this->algorithm = $algorithm;
132 }
133
134 /**
135 * @return string
136 */
137 function getAlgorithm() {
138 return $this->algorithm;
139 }
140
141 function encrypt($password, $salt = null, $member = null) {
142 if(function_exists('hash')) {
143 // Available in PHP 5.1+ only
144 return hash($this->algorithm, $password . $salt);
145 } else {
146 // Fallback to global built-in methods
147 return call_user_func($this->algorithm, $password . $salt);
148 }
149 }
150 }
151
152 /**
153 * Legacy implementation for SilverStripe 2.1 - 2.3,
154 * which had a design flaw in password hashing that caused
155 * the hashes to differ between architectures due to
156 * floating point precision problems in base_convert().
157 * See http://open.silverstripe.org/ticket/3004
158 *
159 * @package sapphire
160 * @subpackage security
161 */
162 class PasswordEncryptor_LegacyPHPHash extends PasswordEncryptor_PHPHash {
163 function encrypt($password, $salt = null, $member = null) {
164 $password = parent::encrypt($password, $salt, $member);
165
166 // Legacy fix: This shortening logic is producing unpredictable results.
167 //
168 // Convert the base of the hexadecimal password to 36 to make it shorter
169 // In that way we can store also a SHA256 encrypted password in just 64
170 // letters.
171 return substr(base_convert($password, 16, 36), 0, 64);
172 }
173
174 function compare($hash1, $hash2) {
175 // Due to flawed base_convert() floating poing precision,
176 // only the first 10 characters are consistently useful for comparisons.
177 return (substr($hash1, 0, 10) === substr($hash2, 0, 10));
178 }
179 }
180
181 /**
182 * Uses MySQL's PASSWORD encryption. Requires an active DB connection.
183 *
184 * @package sapphire
185 * @subpackage security
186 */
187 class PasswordEncryptor_MySQLPassword extends PasswordEncryptor {
188 function encrypt($password, $salt = null, $member = null) {
189 return DB::query(
190 sprintf("SELECT PASSWORD('%s')", Convert::raw2sql($password))
191 )->value();
192 }
193
194 function salt($password, $member = null) {
195 return false;
196 }
197 }
198
199 /**
200 * Uses MySQL's OLD_PASSWORD encyrption. Requires an active DB connection.
201 *
202 * @package sapphire
203 * @subpackage security
204 */
205 class PasswordEncryptor_MySQLOldPassword extends PasswordEncryptor {
206 function encrypt($password, $salt = null, $member = null) {
207 return DB::query(
208 sprintf("SELECT OLD_PASSWORD('%s')", Convert::raw2sql($password))
209 )->value();
210 }
211
212 function salt($password, $member = null) {
213 return false;
214 }
215 }
216
217 /**
218 * Cleartext passwords (used in SilverStripe 2.1).
219 * Also used when Security::$encryptPasswords is set to FALSE.
220 * Not recommended.
221 *
222 * @package sapphire
223 * @subpackage security
224 */
225 class PasswordEncryptor_None extends PasswordEncryptor {
226 function encrypt($password, $salt = null, $member = null) {
227 return $password;
228 }
229
230 function salt($password, $member = null) {
231 return false;
232 }
233 }
234
235 /**
236 * @package sapphire
237 * @subpackage security
238 */
239 class PasswordEncryptor_NotFoundException extends Exception {}