]> CyberLeo.Net >> Repos - Github/YOURLS.git/blob - includes/functions-auth.php
Update phpass
[Github/YOURLS.git] / includes / functions-auth.php
1 <?php
2 /**
3  * Check for valid user via login form or stored cookie. Returns true or an error message
4  *
5  */
6 function yourls_is_valid_user() {
7         // Allow plugins to short-circuit the whole function
8         $pre = yourls_apply_filter( 'shunt_is_valid_user', null );
9         if ( null !== $pre ) {
10                 return $pre;
11         }
12         
13         // $unfiltered_valid : are credentials valid? Boolean value. It's "unfiltered" to allow plugins to eventually filter it.
14         $unfiltered_valid = false;
15
16         // Logout request
17         if( isset( $_GET['action'] ) && $_GET['action'] == 'logout' ) {
18                 yourls_do_action( 'logout' );
19                 yourls_store_cookie( null );
20                 return yourls__( 'Logged out successfully' );
21         }
22         
23         // Check cookies or login request. Login form has precedence.
24
25         yourls_do_action( 'pre_login' );
26
27         // Determine auth method and check credentials
28         if
29                 // API only: Secure (no login or pwd) and time limited token
30                 // ?timestamp=12345678&signature=md5(totoblah12345678)
31                 ( yourls_is_API() &&
32                   isset( $_REQUEST['timestamp'] ) && !empty($_REQUEST['timestamp'] ) &&
33                   isset( $_REQUEST['signature'] ) && !empty($_REQUEST['signature'] )
34                 )
35                 {
36                         yourls_do_action( 'pre_login_signature_timestamp' );
37                         $unfiltered_valid = yourls_check_signature_timestamp();
38                 }
39                 
40         elseif
41                 // API only: Secure (no login or pwd)
42                 // ?signature=md5(totoblah)
43                 ( yourls_is_API() &&
44                   !isset( $_REQUEST['timestamp'] ) &&
45                   isset( $_REQUEST['signature'] ) && !empty( $_REQUEST['signature'] )
46                 )
47                 {
48                         yourls_do_action( 'pre_login_signature' );
49                         $unfiltered_valid = yourls_check_signature();
50                 }
51         
52         elseif
53                 // API or normal: login with username & pwd
54                 ( isset( $_REQUEST['username'] ) && isset( $_REQUEST['password'] )
55                   && !empty( $_REQUEST['username'] ) && !empty( $_REQUEST['password']  ) )
56                 {
57                         yourls_do_action( 'pre_login_username_password' );
58                         $unfiltered_valid = yourls_check_username_password();
59                 }
60         
61         elseif
62                 // Normal only: cookies
63                 ( !yourls_is_API() && 
64                   isset( $_COOKIE[ yourls_cookie_name() ] ) )
65                 {
66                         yourls_do_action( 'pre_login_cookie' );
67                         $unfiltered_valid = yourls_check_auth_cookie();
68                 }
69         
70         // Regardless of validity, allow plugins to filter the boolean and have final word
71         $valid = yourls_apply_filter( 'is_valid_user', $unfiltered_valid );
72
73         // Login for the win!
74         if ( $valid ) {
75                 yourls_do_action( 'login' );
76                 
77                 // (Re)store encrypted cookie if needed
78                 if ( !yourls_is_API() ) {
79                         yourls_store_cookie( YOURLS_USER );
80                         
81                         // Login form : redirect to requested URL to avoid re-submitting the login form on page reload
82                         if( isset( $_REQUEST['username'] ) && isset( $_REQUEST['password'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
83                                 $url = yourls_match_current_protocol(yourls_sanitize_url(sprintf("%s%s", $_SERVER['SERVER_NAME'], $_SERVER['REQUEST_URI'])));
84                                 yourls_redirect( yourls_sanitize_url_safe($url) );
85                         }
86                 }
87                 
88                 // Login successful
89                 return true;
90         }
91         
92         // Login failed
93         yourls_do_action( 'login_failed' );
94
95         if ( isset( $_REQUEST['username'] ) || isset( $_REQUEST['password'] ) ) {
96                 return yourls__( 'Invalid username or password' );
97         } else {
98                 return yourls__( 'Please log in' );
99         }
100 }
101
102 /**
103  * Check auth against list of login=>pwd. Sets user if applicable, returns bool
104  *
105  */
106 function yourls_check_username_password() {
107         global $yourls_user_passwords;
108         if( isset( $yourls_user_passwords[ $_REQUEST['username'] ] ) && yourls_check_password_hash( $_REQUEST['username'], $_REQUEST['password'] ) ) {
109                 yourls_set_user( $_REQUEST['username'] );
110                 return true;
111         }
112         return false;
113 }
114
115 /**
116  * Check a submitted password sent in plain text against stored password which can be a salted hash
117  *
118  */
119 function yourls_check_password_hash( $user, $submitted_password ) {
120         global $yourls_user_passwords;
121         
122         if( !isset( $yourls_user_passwords[ $user ] ) )
123                 return false;
124         
125         if ( yourls_has_phpass_password( $user ) ) {
126                 // Stored password is hashed with phpass
127                 list( , $hash ) = explode( ':', $yourls_user_passwords[ $user ] );
128                 $hash = str_replace( '!', '$', $hash );
129                 return ( yourls_phpass_check( $submitted_password, $hash ) );
130         } else if( yourls_has_md5_password( $user ) ) {
131                 // Stored password is a salted md5 hash: "md5:<$r = rand(10000,99999)>:<md5($r.'thepassword')>"
132                 list( , $salt, ) = explode( ':', $yourls_user_passwords[ $user ] );
133                 return( $yourls_user_passwords[ $user ] == 'md5:'.$salt.':'.md5( $salt . $submitted_password ) );
134         } else {
135                 // Password stored in clear text
136                 return( $yourls_user_passwords[ $user ] == $submitted_password );
137         }
138 }
139
140 /**
141  * Overwrite plaintext passwords in config file with phpassed versions.
142  *
143  * @since 1.7
144  * @param string $config_file Full path to file
145  * @return true if overwrite was successful, an error message otherwise
146  */
147 function yourls_hash_passwords_now( $config_file ) {
148         if( !is_readable( $config_file ) )
149                 return 'cannot read file'; // not sure that can actually happen...
150                 
151         if( !is_writable( $config_file ) )
152                 return 'cannot write file';     
153         
154         // Include file to read value of $yourls_user_passwords
155         // Temporary suppress error reporting to avoid notices about redeclared constants
156         $errlevel = error_reporting();
157         error_reporting( 0 );
158         require $config_file;
159         error_reporting( $errlevel );
160         
161         $configdata = file_get_contents( $config_file );
162         if( $configdata == false )
163                 return 'could not read file';
164
165         $to_hash = 0; // keep track of number of passwords that need hashing
166         foreach ( $yourls_user_passwords as $user => $password ) {
167                 if ( !yourls_has_phpass_password( $user ) && !yourls_has_md5_password( $user ) ) {
168                         $to_hash++;
169                         $hash = yourls_phpass_hash( $password );
170                         // PHP would interpret $ as a variable, so replace it in storage.
171                         $hash = str_replace( '$', '!', $hash );
172                         $quotes = "'" . '"';
173                         $pattern = "/[$quotes]${user}[$quotes]\s*=>\s*[$quotes]" . preg_quote( $password, '/' ) . "[$quotes]/";
174                         $replace = "'$user' => 'phpass:$hash' /* Password encrypted by YOURLS */ ";
175                         $count = 0;
176                         $configdata = preg_replace( $pattern, $replace, $configdata, -1, $count );
177                         // There should be exactly one replacement. Otherwise, fast fail.
178                         if ( $count != 1 ) {
179                                 yourls_debug_log( "Problem with preg_replace for password hash of user $user" );
180                                 return 'preg_replace problem';
181                         }
182                 }
183         }
184         
185         if( $to_hash == 0 )
186                 return 0; // There was no password to encrypt
187         
188         $success = file_put_contents( $config_file, $configdata );
189         if ( $success === FALSE ) {
190                 yourls_debug_log( 'Failed writing to ' . $config_file );
191                 return 'could not write file';
192         }
193         return true;
194 }
195
196 /**
197  * Hash a password using phpass
198  *
199  * @since 1.7
200  * @param string $password password to hash
201  * @return string hashed password
202  */
203 function yourls_phpass_hash( $password ) {
204         $hasher = yourls_phpass_instance();
205         return $hasher->HashPassword( $password );
206 }
207
208 /**
209  * Check a clear password against a phpass hash
210  *
211  * @since 1.7
212  * @param string $password clear (eg submitted in a form) password
213  * @param string $hash hash supposedly generated by phpass
214  * @return bool true if the hash matches the password once hashed by phpass, false otherwise
215  */
216 function yourls_phpass_check( $password, $hash ) {
217         $hasher = yourls_phpass_instance();
218         return $hasher->CheckPassword( $password, $hash );
219 }
220
221 /**
222  * Helper function: create new instance or return existing instance of phpass class
223  *
224  * @since 1.7
225  * @param int $iteration iteration count - 8 is default in phpass
226  * @param bool $portable flag to force portable (cross platform and system independant) hashes - false to use whatever the system can do best
227  * @return object a PasswordHash instance
228  */
229 function yourls_phpass_instance( $iteration = 8, $portable = false ) {
230         $iteration = yourls_apply_filter( 'phpass_new_instance_iteration', $iteration );
231         $portable  = yourls_apply_filter( 'phpass_new_instance_portable', $portable );
232     
233         if( !class_exists( 'Hautelook\Phpass\PasswordHash' ) ) {
234                 require_once( YOURLS_INC.'/phpass/PasswordHash.php' );
235         }
236
237         static $instance = false;
238         if( $instance == false ) {
239                 $instance = new Hautelook\Phpass\PasswordHash( $iteration, $portable );
240         }
241         
242         return $instance;
243 }
244
245
246 /**
247  * Check to see if any passwords are stored as cleartext.
248  * 
249  * @since 1.7
250  * @return bool true if any passwords are cleartext
251  */
252 function yourls_has_cleartext_passwords() {
253         global $yourls_user_passwords;
254         foreach ( $yourls_user_passwords as $user => $pwdata ) {
255                 if ( !yourls_has_md5_password( $user ) && !yourls_has_phpass_password( $user ) ) {
256                         return true;
257                 }
258         }
259         return false;
260 }
261
262 /**
263  * Check if a user has a hashed password
264  *
265  * Check if a user password is 'md5:[38 chars]'.
266  * TODO: deprecate this when/if we have proper user management with password hashes stored in the DB
267  *
268  * @since 1.7
269  * @param string $user user login
270  * @return bool true if password hashed, false otherwise
271  */
272 function yourls_has_md5_password( $user ) {
273         global $yourls_user_passwords;
274         return(    isset( $yourls_user_passwords[ $user ] )
275                 && substr( $yourls_user_passwords[ $user ], 0, 4 ) == 'md5:'
276                     && strlen( $yourls_user_passwords[ $user ] ) == 42 // http://www.google.com/search?q=the+answer+to+life+the+universe+and+everything
277                    );
278 }
279
280 /**
281  * Check if a user's password is hashed with PHPASS.
282  *
283  * Check if a user password is 'phpass:[lots of chars]'.
284  * TODO: deprecate this when/if we have proper user management with password hashes stored in the DB
285  *
286  * @since 1.7
287  * @param string $user user login
288  * @return bool true if password hashed with PHPASS, otherwise false
289  */
290 function yourls_has_phpass_password( $user ) {
291         global $yourls_user_passwords;
292         return( isset( $yourls_user_passwords[ $user ] )
293                 && substr( $yourls_user_passwords[ $user ], 0, 7 ) == 'phpass:'
294         );
295 }
296
297 /**
298  * Check auth against encrypted COOKIE data. Sets user if applicable, returns bool
299  *
300  */
301 function yourls_check_auth_cookie() {
302         global $yourls_user_passwords;
303         foreach( $yourls_user_passwords as $valid_user => $valid_password ) {
304                 if ( yourls_salt( $valid_user ) == $_COOKIE[ yourls_cookie_name() ] ) {
305                         yourls_set_user( $valid_user );
306                         return true;
307                 }
308         }
309         return false;
310 }
311
312 /**
313  * Check auth against signature and timestamp. Sets user if applicable, returns bool
314  *
315  *
316  * @since 1.4.1
317  * @return bool False if signature or timestamp missing or invalid, true if valid
318  */
319 function yourls_check_signature_timestamp() {
320     if(   !isset( $_REQUEST['signature'] ) OR empty( $_REQUEST['signature'] )
321        OR !isset( $_REQUEST['timestamp'] ) OR empty( $_REQUEST['timestamp'] )
322     )
323         return false;
324
325         // Timestamp in PHP : time()
326         // Timestamp in JS: parseInt(new Date().getTime() / 1000)
327     
328         // Check signature & timestamp against all possible users
329         global $yourls_user_passwords;
330         foreach( $yourls_user_passwords as $valid_user => $valid_password ) {
331                 if (
332                         (
333                                 md5( $_REQUEST['timestamp'].yourls_auth_signature( $valid_user ) ) == $_REQUEST['signature']
334                                 or
335                                 md5( yourls_auth_signature( $valid_user ).$_REQUEST['timestamp'] ) == $_REQUEST['signature']
336                         )
337                         &&
338                         yourls_check_timestamp( $_REQUEST['timestamp'] )
339                         ) {
340                         yourls_set_user( $valid_user );
341                         return true;
342                 }
343         }
344
345     // Signature doesn't match known user
346         return false;
347 }
348
349 /**
350  * Check auth against signature. Sets user if applicable, returns bool
351  *
352  * @since 1.4.1
353  * @return bool False if signature missing or invalid, true if valid
354  */
355 function yourls_check_signature() {
356     if( !isset( $_REQUEST['signature'] ) OR empty( $_REQUEST['signature'] ) )
357         return false;
358     
359         // Check signature against all possible users
360     global $yourls_user_passwords;
361         foreach( $yourls_user_passwords as $valid_user => $valid_password ) {
362                 if ( yourls_auth_signature( $valid_user ) == $_REQUEST['signature'] ) {
363                         yourls_set_user( $valid_user );
364                         return true;
365                 }
366         }
367     
368     // Signature doesn't match known user
369         return false;
370 }
371
372 /**
373  * Generate secret signature hash
374  *
375  */
376 function yourls_auth_signature( $username = false ) {
377         if( !$username && defined('YOURLS_USER') ) {
378                 $username = YOURLS_USER;
379         }
380         return ( $username ? substr( yourls_salt( $username ), 0, 10 ) : 'Cannot generate auth signature: no username' );
381 }
382
383 /**
384  * Check if timestamp is not too old
385  *
386  */
387 function yourls_check_timestamp( $time ) {
388         $now = time();
389         // Allow timestamp to be a little in the future or the past -- see Issue 766
390         return yourls_apply_filter( 'check_timestamp', abs( $now - $time ) < YOURLS_NONCE_LIFE, $time );
391 }
392
393 /**
394  * Store new cookie. No $user will delete the cookie.
395  *
396  * @param mixed $user  String, user login, or null to delete cookie
397  */
398 function yourls_store_cookie( $user = null ) {
399
400     // No user will delete the cookie with a cookie time from the past
401         if( !$user ) {
402                 $time = time() - 3600;
403         } else {
404                 $time = time() + YOURLS_COOKIE_LIFE;
405         }
406         
407         $domain   = yourls_apply_filter( 'setcookie_domain',   parse_url( YOURLS_SITE, PHP_URL_HOST ) );
408         $secure   = yourls_apply_filter( 'setcookie_secure',   yourls_is_ssl() );
409         $httponly = yourls_apply_filter( 'setcookie_httponly', true );
410
411         // Some browsers refuse to store localhost cookie
412         if ( $domain == 'localhost' ) 
413                 $domain = '';
414    
415     if ( !headers_sent( $filename, $linenum ) ) {
416         setcookie( yourls_cookie_name(), yourls_salt( $user ), $time, '/', $domain, $secure, $httponly );
417         } else {
418                 // For some reason cookies were not stored: action to be able to debug that
419                 yourls_do_action( 'setcookie_failed', $user );
420         yourls_debug_log( "Could not store cookie: headers already sent in $filename on line $linenum" );
421         }
422 }
423
424 /**
425  * Set user name
426  *
427  */
428 function yourls_set_user( $user ) {
429         if( !defined( 'YOURLS_USER' ) )
430                 define( 'YOURLS_USER', $user );
431 }
432
433 /**
434  * Get YOURLS cookie name
435  *
436  * The name is unique for each install, to prevent mismatch between sho.rt and very.sho.rt -- see #1673
437  *
438  * TODO: when multi user is implemented, the whole cookie stuff should be reworked to allow storing multiple users
439  *
440  * @since 1.7.1
441  * @return string  unique cookie name for a given YOURLS site
442  */
443 function yourls_cookie_name() {
444     return 'yourls_' . yourls_salt( YOURLS_SITE );
445 }