Nico Huber | ee52fbc | 2023-06-24 11:52:57 +0000 | [diff] [blame] | 1 | <?php |
| 2 | /* |
| 3 | * SimpleID |
| 4 | * |
| 5 | * Copyright (C) Kelvin Mo 2007-8 |
| 6 | * |
| 7 | * Includes code Drupal OpenID module (http://drupal.org/project/openid) |
| 8 | * Rowan Kerr <rowan@standardinteractive.com> |
| 9 | * James Walker <james@bryght.com> |
| 10 | * |
| 11 | * Copyright (C) Rowan Kerr and James Walker |
| 12 | * |
| 13 | * This program is free software; you can redistribute it and/or |
| 14 | * modify it under the terms of the GNU General Public |
| 15 | * License as published by the Free Software Foundation; either |
| 16 | * version 2 of the License, or (at your option) any later version. |
| 17 | * |
| 18 | * This program is distributed in the hope that it will be useful, |
| 19 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 20 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 21 | * General Public License for more details. |
| 22 | * |
| 23 | * You should have received a copy of the GNU General Public |
| 24 | * License along with this program; if not, write to the Free |
| 25 | * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
| 26 | * |
| 27 | * $Id$ |
| 28 | */ |
| 29 | |
| 30 | /** |
| 31 | * User functions. |
| 32 | * |
| 33 | * @package simpleid |
| 34 | * @filesource |
| 35 | */ |
| 36 | |
| 37 | /** |
| 38 | * The time the nonce used in the login process will last. |
| 39 | */ |
| 40 | define('SIMPLEID_LOGIN_NONCE_EXPIRES_IN', 3600); |
| 41 | |
| 42 | /** |
| 43 | * The time (in seconds) the auto login cookie will last. This is currently |
| 44 | * set as 2 weeks. |
| 45 | */ |
| 46 | define('SIMPLEID_USER_AUTOLOGIN_EXPIRES_IN', 1209600); |
| 47 | |
| 48 | /** |
| 49 | * This variable holds data on the currently logged-in user. If the user is |
| 50 | * not logged in, this variable is NULL. |
| 51 | * |
| 52 | * @global array $user |
| 53 | */ |
| 54 | $user = NULL; |
| 55 | |
| 56 | /** |
| 57 | * Initialises the user system. Loads data for the currently logged-in user, |
| 58 | * if any. |
| 59 | * |
| 60 | * @param string $q the SimpleID command, if any |
| 61 | */ |
| 62 | function user_init($q = NULL) { |
| 63 | global $user; |
| 64 | global $xtpl; |
| 65 | |
| 66 | log_debug('user_init'); |
| 67 | |
| 68 | $user = NULL; |
| 69 | |
| 70 | // session_name() has to be called before session_set_cookie_params() |
| 71 | session_name(simpleid_cookie_name('sess')); |
| 72 | |
| 73 | // Note the last parameter (httponly) requires PHP 5.2 |
| 74 | session_set_cookie_params(0, get_base_path(), ini_get('session.cookie_domain'), false, true); |
| 75 | session_start(); |
| 76 | |
| 77 | if (isset($_SESSION['user']) && (cache_get('user', $_SESSION['user']) == session_id())) { |
| 78 | $user = user_load($_SESSION['user']); |
| 79 | |
| 80 | // If user has just been actively been authenticated in the previous request, then we |
| 81 | // make it as actively authenticated in this request. |
| 82 | if (isset($_SESSION['user_auth_active']) && $_SESSION['user_auth_active']) { |
| 83 | $user['auth_active'] = true; |
| 84 | unset($_SESSION['user_auth_active']); |
| 85 | } |
| 86 | } else { |
| 87 | if (($q == 'login') || ($q == 'logout')) return; |
| 88 | user_auto_login(); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Attempts to automatically login using credentials presented by the user agent. |
| 94 | * |
| 95 | * The user agent may present various credentials as part of its request. These |
| 96 | * may include cookies and SSL client certificates. This function calls the |
| 97 | * {@link hook_user_auto_login()} hook of enabled extensions to see if any |
| 98 | * of these credentials can be used to automatically login a user. |
| 99 | */ |
| 100 | function user_auto_login() { |
| 101 | global $simpleid_extensions; |
| 102 | |
| 103 | $extensions = $simpleid_extensions; |
| 104 | |
| 105 | if (!in_array('user_cookieauth', $extensions)) $extensions[] = 'user_cookieauth'; |
| 106 | |
| 107 | foreach ($extensions as $extension) { |
| 108 | $test_user = extension_invoke($extension, 'user_auto_login'); |
| 109 | if ($test_user != NULL) { |
| 110 | _user_login($test_user); |
| 111 | } |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * Loads user data for a specified user name. |
| 117 | * |
| 118 | * @param string $uid the name of the user to load |
| 119 | * @return mixed data for the specified user, or NULL if the user name does not |
| 120 | * exist |
| 121 | * @see user_load_from_identity() |
| 122 | */ |
| 123 | function user_load($uid) { |
| 124 | if (store_user_exists($uid)) { |
| 125 | $user = store_user_load($uid); |
| 126 | $user["uid"] = $uid; |
| 127 | |
| 128 | if (isset($user["identity"])) { |
| 129 | $user["local_identity"] = true; |
| 130 | } else { |
| 131 | $user["identity"] = simpleid_url('user/' . rawurlencode($uid)); |
| 132 | $user["local_identity"] = false; |
| 133 | } |
| 134 | |
| 135 | return $user; |
| 136 | } else { |
| 137 | return NULL; |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | /** |
| 142 | * Loads user data for a specified OpenID Identity URI. |
| 143 | * |
| 144 | * @param string $identity the Identity URI of the user to load |
| 145 | * @return mixed data for the specified user, or NULL if the user name does not |
| 146 | * exist |
| 147 | * @see user_load() |
| 148 | */ |
| 149 | function user_load_from_identity($identity) { |
| 150 | $uid = store_get_uid($identity); |
| 151 | if ($uid !== NULL) return user_load($uid); |
| 152 | |
| 153 | return NULL; |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * Stores user data for a specified user name. |
| 158 | * |
| 159 | * @param array $user the user to save |
| 160 | */ |
| 161 | function user_save($user) { |
| 162 | $uid = $user['uid']; |
| 163 | store_user_save($uid, $user, array('uid', 'identity', 'pass')); |
| 164 | } |
| 165 | |
| 166 | /** |
| 167 | * Attempts to log in a user, using the user name and password specified in the |
| 168 | * HTTP request. |
| 169 | */ |
| 170 | function user_login() { |
| 171 | global $user, $GETPOST; |
| 172 | |
| 173 | // If the user is already logged in, return |
| 174 | if (isset($user['uid'])) openid_indirect_response(simpleid_url(), ''); |
| 175 | |
| 176 | // Require HTTPS or return an error |
| 177 | check_https('error', true); |
| 178 | |
| 179 | $destination = (isset($GETPOST['destination'])) ? $GETPOST['destination'] : ''; |
| 180 | $state = (isset($GETPOST['s'])) ? $GETPOST['s'] : ''; |
| 181 | $fixed_uid = (isset($_POST['fixed_uid'])) ? $_POST['name'] : NULL; |
| 182 | $mode = $_POST['mode']; |
| 183 | |
| 184 | $query = ($state) ? 's=' . rawurlencode($state) : ''; |
| 185 | |
| 186 | if (isset($_POST['op']) && $_POST['op'] == t('Cancel')) { |
| 187 | global $version; |
| 188 | |
| 189 | $request = unpickle($state); |
| 190 | $version = openid_get_version($request); |
| 191 | |
| 192 | if (isset($request['openid.return_to'])) { |
| 193 | $return_to = $request['openid.return_to']; |
| 194 | $response = simpleid_checkid_error(FALSE); |
| 195 | simpleid_assertion_response($response, $return_to); |
| 196 | } else { |
| 197 | indirect_fatal_error(t('Login cancelled without a proper OpenID request.')); |
| 198 | } |
| 199 | return; |
| 200 | } |
| 201 | |
| 202 | if (!isset($_POST['mode']) || !in_array($_POST['mode'], array('credentials', 'otp'))) { |
| 203 | set_message(t('SimpleID detected a potential security attack on your log in. Please log in again.')); |
| 204 | user_login_form($destination, $state, $fixed_uid); |
| 205 | return; |
| 206 | } |
| 207 | |
| 208 | if (!isset($_POST['nonce'])) { |
| 209 | if (isset($_POST['destination'])) { |
| 210 | // User came from a log in form. |
| 211 | set_message(t('You seem to be attempting to log in from another web page. You must use this page to log in.')); |
| 212 | } |
| 213 | user_login_form($destination, $state, $fixed_uid, $mode); |
| 214 | return; |
| 215 | } |
| 216 | |
| 217 | $time = strtotime(substr($_POST['nonce'], 0, 20)); |
| 218 | // Some old versions of PHP does not recognise the T in the ISO 8601 date. We may need to convert the T to a space |
| 219 | if (($time == -1) || ($time === FALSE)) $time = strtotime(strtr(substr($_POST['nonce'], 0, 20), 'T', ' ')); |
| 220 | $nonce = cache_get('user-nonce', $_POST['nonce']); |
| 221 | |
| 222 | if (!$nonce) { |
| 223 | log_warn('Login attempt: Nonce ' . $_POST['nonce'] . ' not issued or is being reused.'); |
| 224 | set_message(t('SimpleID detected a potential security attack on your log in. Please log in again.')); |
| 225 | user_login_form($destination, $state, $fixed_uid, $mode); |
| 226 | return; |
| 227 | } elseif ($time < time() - SIMPLEID_LOGIN_NONCE_EXPIRES_IN) { |
| 228 | log_notice('Login attempt: Nonce ' . $_POST['nonce'] . ' expired.'); |
| 229 | set_message(t('The log in page has expired. Please log in again.')); |
| 230 | user_login_form($destination, $state, $fixed_uid, $mode); |
| 231 | return; |
| 232 | } elseif ($nonce['mode'] != $mode) { |
| 233 | log_warn('Login attempt: Mode saved with nonce ' . $_POST['nonce'] . ' (' . $nonce['mode'] . ') does not match ' . $mode); |
| 234 | set_message(t('SimpleID detected a potential security attack on your log in. Please log in again.')); |
| 235 | user_login_form($destination, $state, $fixed_uid, $mode); |
| 236 | } else { |
| 237 | cache_delete('user-nonce', $_POST['nonce']); |
| 238 | } |
| 239 | |
| 240 | switch ($mode) { |
| 241 | case 'credentials': |
| 242 | if (!isset($_POST['name'])) $_POST['name'] = ''; |
| 243 | if (!isset($_POST['pass'])) $_POST['pass'] = ''; |
| 244 | |
| 245 | if (($_POST['name'] == '') || ($_POST['pass'] == '')) { |
| 246 | if (isset($_POST['destination'])) { |
| 247 | // User came from a log in form. |
| 248 | set_message(t('You need to supply the user name and the password in order to log in.')); |
| 249 | } |
| 250 | if (isset($_POST['nonce'])) cache_delete('user-nonce', $_POST['nonce']); |
| 251 | user_login_form($destination, $state, $fixed_uid); |
| 252 | return; |
| 253 | } |
| 254 | |
| 255 | if (user_verify_credentials($_POST['name'], $_POST) === false) { |
| 256 | set_message(t('The user name or password is not correct.')); |
| 257 | user_login_form($destination, $state, $fixed_uid); |
| 258 | return; |
| 259 | } |
| 260 | |
| 261 | $test_user = user_load($_POST['name']); |
| 262 | if (isset($test_user['otp']) && ($test_user['otp']['type'] != 'recovery')) { |
| 263 | log_info('One time password required'); |
| 264 | user_login_form($destination, $state, $test_user['uid'], 'otp'); |
| 265 | return; |
| 266 | } |
| 267 | break; |
| 268 | case 'otp': |
| 269 | if (!isset($_POST['otp']) || ($_POST['otp'] == '')) { |
| 270 | set_message(t('You need to enter the verification code in order to log in.')); |
| 271 | if (isset($_POST['nonce'])) cache_delete('user-nonce', $_POST['nonce']); |
| 272 | user_login_form($destination, $state, $nonce['uid'], 'otp'); |
| 273 | return; |
| 274 | } |
| 275 | |
| 276 | $test_user = user_load($nonce['uid']); |
| 277 | |
| 278 | if (user_verify_otp($test_user['otp'], $_POST['otp']) === false) { |
| 279 | set_message(t('The verification code is not correct.')); |
| 280 | user_login_form($destination, $state, $nonce['uid'], 'otp'); |
| 281 | return; |
| 282 | } |
| 283 | user_save($test_user); // Save the drift |
| 284 | |
| 285 | break; |
| 286 | } |
| 287 | |
| 288 | _user_login($test_user, true); |
| 289 | |
| 290 | openid_indirect_response(simpleid_url($destination, $query), ''); |
| 291 | } |
| 292 | |
| 293 | /** |
| 294 | * Verifies a set of credentials for a specified user. |
| 295 | * |
| 296 | * A set of credentials comprises: |
| 297 | * |
| 298 | * - A user name |
| 299 | * - Some kind of verifying information, such as a plaintext password, a hashed |
| 300 | * password (e.g. digest) or some other kind of identifying information. |
| 301 | * |
| 302 | * The user name is passed to this function using the $uid parameter. The user |
| 303 | * name may or may not exist. If the user name does not exist, this function |
| 304 | * <strong>must</strong> return false. |
| 305 | * |
| 306 | * The credentials are supplied as an array using the $credentials parameter. |
| 307 | * Typically this array will be a subset of the $_POST superglobal passed to the |
| 308 | * {@link user_login()} function. Thus it will generally contain the keys 'pass' and |
| 309 | * 'digest'. |
| 310 | * |
| 311 | * This function calls the {@link hook_user_verify_credentials()} hook to |
| 312 | * check whether the credentials supplied matches the credentials |
| 313 | * for the specified user in the store. |
| 314 | * |
| 315 | * @param string $uid the name of the user to verify |
| 316 | * @param array $credentials the credentials supplied by the browser |
| 317 | * @return bool whether the credentials supplied matches those for the specified |
| 318 | * user |
| 319 | */ |
| 320 | function user_verify_credentials($uid, $credentials) { |
| 321 | global $simpleid_extensions; |
| 322 | |
| 323 | $extensions = $simpleid_extensions; |
| 324 | |
| 325 | if (!in_array('user_passauth', $extensions)) $extensions[] = 'user_passauth'; |
| 326 | |
| 327 | foreach ($extensions as $extension) { |
| 328 | $result = extension_invoke($extension, 'user_verify_credentials', $uid, $credentials); |
| 329 | if ($result === true) { |
| 330 | return true; |
| 331 | } |
| 332 | } |
| 333 | |
| 334 | return false; |
| 335 | } |
| 336 | |
| 337 | /** |
| 338 | * Verifies a one time password (OTP) specified by the user. |
| 339 | * |
| 340 | * This function compares an OTP supplied by a user with the OTP |
| 341 | * calculated based on the current time and the parameters of the |
| 342 | * algorithm. The parameters, such as the secret key, are supplied |
| 343 | * using in $params. These parameters are typically stored for each |
| 344 | * user in the user store. |
| 345 | * |
| 346 | * To allow for clocks going out of sync, the current time will be |
| 347 | * by a number (in time steps) specified in $params['drift']. If |
| 348 | * the OTP supplied by the user is accepted, $params['drift'] will |
| 349 | * be also be updated with the latest difference. |
| 350 | * |
| 351 | * To allow for network delay, the function will accepts OTPs which |
| 352 | * is a number of time steps away from the OTP calculated from the |
| 353 | * adjusted time. The maximum number of time steps is specified in |
| 354 | * the $max_drift parameter. |
| 355 | * |
| 356 | * @param array &$params the OTP parameters stored |
| 357 | * @param string $code the OTP supplied by the user |
| 358 | * @param int $max_drift the maximum drift allowed for network delay, in |
| 359 | * time steps |
| 360 | * @return bool whether the OTP supplied matches the OTP generated based on |
| 361 | * the specified parameters, within the maximum drift |
| 362 | */ |
| 363 | function user_verify_otp(&$params, $code, $max_drift = 1) { |
| 364 | switch ($params['type']) { |
| 365 | case 'totp': |
| 366 | $time = time(); |
| 367 | |
| 368 | $test_code = user_totp($params['secret'], $time, $params['period'], $params['drift'], $params['algorithm'], $params['digits']); |
| 369 | |
| 370 | if ($test_code == intval($code)) return true; |
| 371 | |
| 372 | for ($i = -$max_drift; $i <= $max_drift; $i++) { |
| 373 | $test_code = user_totp($params['secret'], $time, $params['period'], $params['drift'] + $i, $params['algorithm'], $params['digits']); |
| 374 | if ($test_code == intval($code)) { |
| 375 | $params['drift'] = $i; |
| 376 | return true; |
| 377 | } |
| 378 | } |
| 379 | return false; |
| 380 | break; |
| 381 | default: |
| 382 | return false; |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | |
| 387 | /** |
| 388 | * Sets the user specified by the parameter as the active user. |
| 389 | * |
| 390 | * @param array $login_user the user to log in |
| 391 | * @param bool $auth_active whether the user has been actively authenticated |
| 392 | * in this session |
| 393 | * |
| 394 | */ |
| 395 | function _user_login($login_user, $auth_active = false) { |
| 396 | global $user; |
| 397 | |
| 398 | if ($auth_active) { |
| 399 | // Set the current authentication time |
| 400 | $login_user['auth_time'] = time(); |
| 401 | user_save($login_user); |
| 402 | |
| 403 | // Set user has been actively authenticated this and the next request only |
| 404 | $login_user['auth_active'] = true; |
| 405 | $_SESSION['user_auth_active'] = true; |
| 406 | log_info('Login successful: ' . $login_user['uid'] . '['. gmstrftime('%Y-%m-%dT%H:%M:%SZ', $login_user['auth_time']) . ']'); |
| 407 | |
| 408 | } |
| 409 | |
| 410 | $user = $login_user; |
| 411 | $_SESSION['user'] = $login_user['uid']; |
| 412 | cache_set('user', $login_user['uid'], session_id()); |
| 413 | |
| 414 | |
| 415 | if ($auth_active) { |
| 416 | if (isset($_POST['autologin']) && ($_POST['autologin'] == 1)) user_cookieauth_create_cookie(); |
| 417 | } |
| 418 | } |
| 419 | |
| 420 | /** |
| 421 | * Attempts to log out a user and returns to the login form. |
| 422 | * |
| 423 | * @param string $destination the destination value to be included in the |
| 424 | * login form |
| 425 | */ |
| 426 | function user_logout($destination = NULL) { |
| 427 | global $user, $GETPOST; |
| 428 | |
| 429 | // Require HTTPS, redirect if necessary |
| 430 | check_https('redirect', true); |
| 431 | |
| 432 | $state = (isset($GETPOST['s'])) ? $GETPOST['s'] : ''; |
| 433 | if ($destination == NULL) { |
| 434 | if (isset($GETPOST['destination'])) { |
| 435 | $destination = $GETPOST['destination']; |
| 436 | } else { |
| 437 | $destination = ''; |
| 438 | } |
| 439 | } |
| 440 | |
| 441 | _user_logout(); |
| 442 | |
| 443 | set_message(t('You have been logged out.')); |
| 444 | |
| 445 | user_login_form($destination, $state); |
| 446 | } |
| 447 | |
| 448 | /** |
| 449 | * Logs out the user by deleting the relevant session information. |
| 450 | */ |
| 451 | function _user_logout() { |
| 452 | global $user; |
| 453 | |
| 454 | $uid = $user['uid']; |
| 455 | |
| 456 | user_cookieauth_invalidate(); |
| 457 | session_destroy(); |
| 458 | |
| 459 | cache_delete('user', $uid); |
| 460 | unset($_SESSION['user']); |
| 461 | $user = NULL; |
| 462 | |
| 463 | log_info('Logout successful: ' . $uid); |
| 464 | } |
| 465 | |
| 466 | /** |
| 467 | * Displays a user login or a login verification form. |
| 468 | * |
| 469 | * @param string $destination the SimpleID location to which the user is directed |
| 470 | * if login is successful |
| 471 | * @param string $state the current SimpleID state, if required by the location |
| 472 | * @param string $fixed_uid the user name to be included in the login form; if NULL, the user |
| 473 | * is asked to supply the user name. If $mode is otp this cannot be null |
| 474 | * @param string $mode either credentials (login form) or otp (login verification |
| 475 | * form) |
| 476 | */ |
| 477 | function user_login_form($destination = '', $state = NULL, $fixed_uid = NULL, $mode = 'credentials') { |
| 478 | global $xtpl; |
| 479 | |
| 480 | // Require HTTPS, redirect if necessary |
| 481 | check_https('redirect', true); |
| 482 | |
| 483 | if ($state) { |
| 484 | $xtpl->assign('state', htmlspecialchars($state, ENT_QUOTES, 'UTF-8')); |
| 485 | $xtpl->assign('cancel_button', t('Cancel')); |
| 486 | $xtpl->parse('main.login.state'); |
| 487 | } |
| 488 | |
| 489 | cache_expire(array('user-nonce' => SIMPLEID_LOGIN_NONCE_EXPIRES_IN)); |
| 490 | $nonce = openid_nonce(); |
| 491 | cache_set('user-nonce', $nonce, array('mode' => $mode, 'uid' => $fixed_uid)); |
| 492 | |
| 493 | $base_path = get_base_path(); |
| 494 | $xtpl->assign('javascript', '<script src="' . $base_path . 'html/user-login.js" type="text/javascript"></script>'); |
| 495 | |
| 496 | header('X-Frame-Options: DENY'); |
| 497 | |
| 498 | switch ($mode) { |
| 499 | case 'credentials': |
| 500 | $security_class = (SIMPLEID_ALLOW_AUTOCOMPLETE) ? 'allow-autocomplete ' : ''; |
| 501 | if (is_https()) { |
| 502 | $security_class .= 'secure'; |
| 503 | $xtpl->assign('security_message', t('Secure login using <strong>HTTPS</strong>.')); |
| 504 | } elseif (SIMPLEID_ALLOW_PLAINTEXT) { |
| 505 | $security_class .= 'unsecure'; |
| 506 | $xtpl->assign('security_message', t('<strong>WARNING:</strong> Your password will be sent to SimpleID as plain text.')); |
| 507 | } |
| 508 | $xtpl->assign('security_class', $security_class); |
| 509 | $xtpl->parse('main.login.login_security'); |
| 510 | |
| 511 | extension_invoke_all('user_login_form', $destination, $state); |
| 512 | |
| 513 | $xtpl->assign('name_label', t('User name:')); |
| 514 | $xtpl->assign('pass_label', t('Password:')); |
| 515 | $xtpl->assign('autologin_label', t('Remember me on this computer for two weeks.')); |
| 516 | |
| 517 | if ($fixed_uid == NULL) { |
| 518 | $xtpl->parse('main.login.credentials.input_uid'); |
| 519 | } else { |
| 520 | $xtpl->assign('uid', htmlspecialchars($fixed_uid, ENT_QUOTES, 'UTF-8')); |
| 521 | $xtpl->parse('main.login.credentials.fixed_uid'); |
| 522 | } |
| 523 | |
| 524 | $xtpl->parse('main.login.credentials'); |
| 525 | $xtpl->assign('submit_button', t('Log in')); |
| 526 | $xtpl->assign('title', t('Log In')); |
| 527 | break; |
| 528 | case 'otp': |
| 529 | // Note this is called from user_login(), so $_POST is always filled |
| 530 | $xtpl->assign('otp_instructions_label', t('To verify your identity, enter the verification code.')); |
| 531 | $xtpl->assign('otp_recovery_label', t('If you have lost your verification code, you can <a href="!url">recover your account</a>.', |
| 532 | array('!url' => 'http://simpleid.org/docs/1/common-problems/#otp') |
| 533 | )); |
| 534 | |
| 535 | $xtpl->assign('otp_label', t('Verification code:')); |
| 536 | $xtpl->assign('autologin', (isset($_POST['autologin']) && ($_POST['autologin'] == 1)) ? '1' : '0'); |
| 537 | $xtpl->parse('main.login.otp'); |
| 538 | |
| 539 | $xtpl->assign('submit_button', t('Verify')); |
| 540 | $xtpl->assign('title', t('Enter Verification Code')); |
| 541 | default: |
| 542 | } |
| 543 | |
| 544 | |
| 545 | $xtpl->assign('mode', $mode); |
| 546 | $xtpl->assign('page_class', 'dialog-page'); |
| 547 | $xtpl->assign('destination', htmlspecialchars($destination, ENT_QUOTES, 'UTF-8')); |
| 548 | $xtpl->assign('nonce', htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8')); |
| 549 | |
| 550 | $xtpl->parse('main.login'); |
| 551 | $xtpl->parse('main.framekiller'); |
| 552 | $xtpl->parse('main'); |
| 553 | $xtpl->out('main'); |
| 554 | } |
| 555 | |
| 556 | /** |
| 557 | * Displays the page used to set up login verification using one-time |
| 558 | * passwords. |
| 559 | */ |
| 560 | function user_otp_page() { |
| 561 | global $xtpl, $user; |
| 562 | |
| 563 | // Require HTTPS, redirect if necessary |
| 564 | check_https('redirect', true); |
| 565 | |
| 566 | if ($user == NULL) { |
| 567 | user_login_form('my/profile'); |
| 568 | return; |
| 569 | } |
| 570 | |
| 571 | if ($_POST['op'] == t('Disable')) { |
| 572 | if (!isset($_POST['tk']) || !validate_form_token($_POST['tk'], 'dashboard_otp')) { |
| 573 | set_message(t('SimpleID detected a potential security attack. Please try again.')); |
| 574 | page_dashboard(); |
| 575 | return; |
| 576 | } |
| 577 | |
| 578 | if (isset($user['otp'])) { |
| 579 | unset($user['otp']); |
| 580 | user_save($user); |
| 581 | } |
| 582 | set_message('Login verification has been disabled.'); |
| 583 | page_dashboard(); |
| 584 | return; |
| 585 | } elseif ($_POST['op'] == t('Verify')) { |
| 586 | $params = $_SESSION['otp_setup']; |
| 587 | |
| 588 | if (!isset($_POST['tk']) || !validate_form_token($_POST['tk'], 'otp')) { |
| 589 | set_message(t('SimpleID detected a potential security attack. Please try again.')); |
| 590 | page_dashboard(); |
| 591 | return; |
| 592 | } elseif (!isset($_POST['otp']) || ($_POST['otp'] == '')) { |
| 593 | set_message(t('You need to enter the verification code to complete enabling login verification.')); |
| 594 | } elseif (user_verify_otp($params, $_POST['otp'], 10) === false) { |
| 595 | set_message(t('The verification code is not correct.')); |
| 596 | } else { |
| 597 | unset($_SESSION['otp_setup']); |
| 598 | $user['otp'] = $params; |
| 599 | user_save($user); |
| 600 | |
| 601 | set_message('Login verification has been enabled.'); |
| 602 | page_dashboard(); |
| 603 | return; |
| 604 | } |
| 605 | } else { |
| 606 | $params = array( |
| 607 | 'type' => 'totp', |
| 608 | 'secret' => random_bytes(10), |
| 609 | 'algorithm' => 'sha1', |
| 610 | 'digits' => 6, |
| 611 | 'period' => 30, |
| 612 | 'drift' => 0, |
| 613 | ); |
| 614 | $_SESSION['otp_setup'] = $params; |
| 615 | } |
| 616 | |
| 617 | $code = strtr(bignum_val(bignum_new($params['secret'], 256), 32), '0123456789abcdefghijklmnopqrstuv', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'); |
| 618 | for ($i = 0; $i < strlen($code); $i += 4) { |
| 619 | $xtpl->assign('secret' . ($i + 1), substr($code, $i, 4)); |
| 620 | } |
| 621 | |
| 622 | $url = 'otpauth://totp/SimpleID?secret=' . $code . '&digits=' . $params['digits'] . '&period=' . $params['period']; |
| 623 | $xtpl->assign('qr', addslashes($url)); |
| 624 | |
| 625 | $xtpl->assign('about_otp', t('Login verification adds an extra layer of protection to your account. When enabled, you will need to enter an additional security code whenever you log into SimpleID.')); |
| 626 | $xtpl->assign('otp_warning', t('<strong>WARNING:</strong> If you enable login verification and lose your authenticator app, you will need to <a href="!url">edit your identity file manually</a> before you can log in again.', |
| 627 | array('!url' => 'http://simpleid.org/docs/1/common-problems/#otp') |
| 628 | )); |
| 629 | |
| 630 | $xtpl->assign('setup_otp', t('To set up login verification, following these steps.')); |
| 631 | $xtpl->assign('download_app', t('Download an authenticator app that supports TOTP for your smartphone, such as Google Authenticator.')); |
| 632 | $xtpl->assign('add_account', t('Add your SimpleID account to authenticator app using this key. If you are viewing this page on your smartphone you can use <a href="!url">this link</a> or scan the QR code to add your account.', |
| 633 | array('!url' => $url) |
| 634 | )); |
| 635 | $xtpl->assign('verify_code', t('To check that your account has been added properly, enter the verification code from your phone into the box below, and click Verify.')); |
| 636 | |
| 637 | $xtpl->assign('token', get_form_token('otp')); |
| 638 | $xtpl->assign('otp_label', t('Verification code:')); |
| 639 | $xtpl->assign('submit_button', t('Verify')); |
| 640 | |
| 641 | $xtpl->assign('page_class', 'dialog-page'); |
| 642 | $xtpl->assign('title', t('Login Verification')); |
| 643 | |
| 644 | $xtpl->parse('main.otp'); |
| 645 | $xtpl->parse('main.framekiller'); |
| 646 | |
| 647 | $xtpl->parse('main'); |
| 648 | $xtpl->out('main'); |
| 649 | |
| 650 | } |
| 651 | |
| 652 | |
| 653 | /** |
| 654 | * Returns the user's public page. |
| 655 | * |
| 656 | * @param string $uid the user ID |
| 657 | */ |
| 658 | function user_public_page($uid = NULL) { |
| 659 | global $xtpl, $user; |
| 660 | |
| 661 | $xtpl->assign('title', t('User Page')); |
| 662 | if ($uid == NULL) { |
| 663 | header_response_code('400 Bad Request'); |
| 664 | set_message(t('No user specified.')); |
| 665 | } else { |
| 666 | $user = user_load($uid); |
| 667 | |
| 668 | if ($user == NULL) { |
| 669 | header_response_code('404 Not Found'); |
| 670 | set_message(t('User %uid not found.', array('%uid' => $uid))); |
| 671 | } else { |
| 672 | header('Vary: Accept'); |
| 673 | |
| 674 | $content_type = negotiate_content_type(array('text/html', 'application/xml', 'application/xhtml+xml', 'application/xrds+xml')); |
| 675 | |
| 676 | if ($content_type == 'application/xrds+xml') { |
| 677 | user_xrds($uid); |
| 678 | return; |
| 679 | } else { |
| 680 | header('X-XRDS-Location: ' . simpleid_url('xrds/' . rawurlencode($uid))); |
| 681 | |
| 682 | set_message(t('This is the user %uid\'s SimpleID page. It contains hidden information for the use by OpenID consumers.', array('%uid' => $uid))); |
| 683 | |
| 684 | $xtpl->assign('title', htmlspecialchars($uid, ENT_QUOTES, 'UTF-8')); |
| 685 | $xtpl->assign('provider', htmlspecialchars(simpleid_url(), ENT_QUOTES, 'UTF-8')); |
| 686 | $xtpl->assign('xrds', htmlspecialchars(simpleid_url('xrds/' . rawurlencode($uid)), ENT_QUOTES, 'UTF-8')); |
| 687 | if ($user["local_identity"]) { |
| 688 | $xtpl->assign('local_id', htmlspecialchars($user["identity"], ENT_QUOTES, 'UTF-8')); |
| 689 | } |
| 690 | } |
| 691 | } |
| 692 | } |
| 693 | |
| 694 | $xtpl->parse('main.provider'); |
| 695 | if ($user["local_identity"]) $xtpl->parse('main.local_id'); |
| 696 | $xtpl->parse('main'); |
| 697 | $xtpl->out('main'); |
| 698 | } |
| 699 | |
| 700 | /** |
| 701 | * Returns the public page for a private personal ID. |
| 702 | * |
| 703 | * @param string $ppid the PPID |
| 704 | */ |
| 705 | function user_ppid_page($ppid = NULL) { |
| 706 | global $xtpl; |
| 707 | |
| 708 | header('Vary: Accept'); |
| 709 | |
| 710 | $content_type = negotiate_content_type(array('text/html', 'application/xml', 'application/xhtml+xml', 'application/xrds+xml')); |
| 711 | |
| 712 | if (($content_type == 'application/xrds+xml') || ($_GET['format'] == 'xrds')) { |
| 713 | header('Content-Type: application/xrds+xml'); |
| 714 | header('Content-Disposition: inline; filename=yadis.xml'); |
| 715 | |
| 716 | $xtpl->assign('simpleid_base_url', htmlspecialchars(simpleid_url(), ENT_QUOTES, 'UTF-8')); |
| 717 | $xtpl->parse('xrds.user_xrds'); |
| 718 | $xtpl->parse('xrds'); |
| 719 | $xtpl->out('xrds'); |
| 720 | return; |
| 721 | } else { |
| 722 | header('X-XRDS-Location: ' . simpleid_url('ppid/' . rawurlencode($ppid), 'format=xrds')); |
| 723 | |
| 724 | $xtpl->assign('title', t('Private Personal Identifier')); |
| 725 | |
| 726 | set_message(t('This is a private personal identifier.')); |
| 727 | |
| 728 | $xtpl->parse('main'); |
| 729 | $xtpl->out('main'); |
| 730 | } |
| 731 | } |
| 732 | |
| 733 | /** |
| 734 | * Returns the user's public XRDS page. |
| 735 | * |
| 736 | * @param string $uid the user ID |
| 737 | */ |
| 738 | function user_xrds($uid) { |
| 739 | global $xtpl; |
| 740 | |
| 741 | $user = user_load($uid); |
| 742 | |
| 743 | if ($user != NULL) { |
| 744 | header('Content-Type: application/xrds+xml'); |
| 745 | header('Content-Disposition: inline; filename=yadis.xml'); |
| 746 | |
| 747 | if (($user != NULL) && ($user["local_identity"])) { |
| 748 | $xtpl->assign('local_id', htmlspecialchars($user["identity"], ENT_QUOTES, 'UTF-8')); |
| 749 | $xtpl->parse('xrds.user_xrds.local_id'); |
| 750 | $xtpl->parse('xrds.user_xrds.local_id2'); |
| 751 | } |
| 752 | |
| 753 | $xtpl->assign('simpleid_base_url', htmlspecialchars(simpleid_url(), ENT_QUOTES, 'UTF-8')); |
| 754 | $xtpl->parse('xrds.user_xrds'); |
| 755 | $xtpl->parse('xrds'); |
| 756 | $xtpl->out('xrds'); |
| 757 | } else { |
| 758 | if (substr(PHP_SAPI, 0,3) === 'cgi') { |
| 759 | header('Status: 404 Not Found'); |
| 760 | } else { |
| 761 | header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); |
| 762 | } |
| 763 | |
| 764 | set_message('User <strong>' . htmlspecialchars($uid, ENT_QUOTES, 'UTF-8') . '</strong> not found.'); |
| 765 | $xtpl->parse('main'); |
| 766 | $xtpl->out('main'); |
| 767 | } |
| 768 | } |
| 769 | |
| 770 | /** |
| 771 | * Returns a block containing OpenID Connect user information. |
| 772 | * |
| 773 | * @return array the OpenID Connect user information block |
| 774 | */ |
| 775 | function _user_page_profile() { |
| 776 | global $user; |
| 777 | |
| 778 | $html = '<p>' . t('SimpleID may, with your consent, send the following information to sites which supports OpenID Connect.') . '</p>'; |
| 779 | $html .= '<p>' . t('To change these, <a href="!url">edit your identity file</a>.', array('!url' => 'http://simpleid.org/docs/1/identity-files/')) . '</p>'; |
| 780 | |
| 781 | $html .= "<table><tr><th>" . t('Member') . "</th><th>" . t('Value') . "</th></tr>"; |
| 782 | |
| 783 | if (isset($user['user_info'])) { |
| 784 | foreach ($user['user_info'] as $member => $value) { |
| 785 | if (is_array($value)) { |
| 786 | foreach ($value as $submember => $subvalue) { |
| 787 | $html .= "<tr><td>" . htmlspecialchars($member, ENT_QUOTES, 'UTF-8') . " (" .htmlspecialchars($submember, ENT_QUOTES, 'UTF-8') . ")</td><td>" . htmlspecialchars($subvalue, ENT_QUOTES, 'UTF-8') . "</td></tr>"; |
| 788 | } |
| 789 | } else { |
| 790 | $html .= "<tr><td>" . htmlspecialchars($member, ENT_QUOTES, 'UTF-8') . "</td><td>" . htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . "</td></tr>"; |
| 791 | } |
| 792 | } |
| 793 | } |
| 794 | |
| 795 | $html .= "</table>"; |
| 796 | |
| 797 | return array(array( |
| 798 | 'id' => 'userinfo', |
| 799 | 'title' => t('OpenID Connect'), |
| 800 | 'content' => $html |
| 801 | )); |
| 802 | } |
| 803 | |
| 804 | /** |
| 805 | * Set up the user section in the header, showing the currently logged in user. |
| 806 | * |
| 807 | * @param string $state the SimpleID state to retain once the user has logged out, |
| 808 | * if required. |
| 809 | */ |
| 810 | function user_header($state = NULL) { |
| 811 | global $user; |
| 812 | global $xtpl; |
| 813 | |
| 814 | if ($user != NULL) { |
| 815 | $xtpl->assign('uid', htmlspecialchars($user['uid'], ENT_QUOTES, 'UTF-8')); |
| 816 | $xtpl->assign('identity', htmlspecialchars($user['identity'], ENT_QUOTES, 'UTF-8')); |
| 817 | if ($state != NULL) { |
| 818 | $xtpl->assign('url', htmlspecialchars(simpleid_url('logout', 'destination=continue&s=' . rawurlencode($state), true))); |
| 819 | $xtpl->assign('logout', t('Log out and log in as a different user')); |
| 820 | } else { |
| 821 | $xtpl->assign('url', htmlspecialchars(simpleid_url('logout', '', true))); |
| 822 | $xtpl->assign('logout', t('Log out')); |
| 823 | } |
| 824 | $xtpl->parse('main.user.logout'); |
| 825 | $xtpl->parse('main.user'); |
| 826 | } |
| 827 | } |
| 828 | |
| 829 | /** |
| 830 | * Verifies a set of credentials using the default user name-password authentication |
| 831 | * method. |
| 832 | * |
| 833 | * @param string $uid the name of the user to verify |
| 834 | * @param array $credentials the credentials supplied by the browser |
| 835 | * @return bool whether the credentials supplied matches those for the specified |
| 836 | * user |
| 837 | */ |
| 838 | function user_passauth_user_verify_credentials($uid, $credentials) { |
| 839 | $allowed_algorithms = array('md5', 'sha1'); |
| 840 | if (function_exists('hash_algos')) $allowed_algorithms = array_merge($allowed_algorithms, hash_algos()); |
| 841 | if (function_exists('hash_pbkdf2')) $allowed_algorithms[] = 'pbkdf2'; |
| 842 | |
| 843 | $test_user = user_load($uid); |
| 844 | |
| 845 | if ($test_user == NULL) return false; |
| 846 | |
| 847 | $hash_function_salt = explode(':', $test_user['pass'], 3); |
| 848 | |
| 849 | $hash = $hash_function_salt[0]; |
| 850 | $function = (isset($hash_function_salt[1])) ? $hash_function_salt[1] : 'md5'; |
| 851 | if (!in_array($function, $allowed_algorithms)) $function = 'md5'; |
| 852 | $salt_suffix = (isset($hash_function_salt[2])) ? ':' . $hash_function_salt[2] : ''; |
| 853 | |
| 854 | switch ($function) { |
| 855 | case 'pbkdf2': |
| 856 | list ($algo, $iterations, $salt) = explode(':', $hash_function_salt[2]); |
| 857 | $length = (function_exists('hash')) ? strlen(hash($algo, '')) : 0; |
| 858 | $test_hash = hash_pbkdf2($algo, $credentials['pass'], $salt, $iterations, $length); |
| 859 | break; |
| 860 | case 'md5': |
| 861 | case 'sha1': |
| 862 | $test_hash = call_user_func($function, $credentials['pass'] . $salt_suffix); |
| 863 | break; |
| 864 | default: |
| 865 | $test_hash = hash($function, $credentials['pass'] . $salt_suffix); |
| 866 | } |
| 867 | |
| 868 | return secure_compare($test_hash, $hash); |
| 869 | } |
| 870 | |
| 871 | /** |
| 872 | * Creates a auto login cookie. The login cookie will be based on the |
| 873 | * current log in user. |
| 874 | * |
| 875 | * @param string $id the ID of the series of auto login cookies, Cookies |
| 876 | * belonging to the same user and computer have the same ID. If none is specified, |
| 877 | * one will be generated |
| 878 | * @param int $expires the time at which the cookie will expire. If none is specified |
| 879 | * the time specified in {@link SIMPLEID_USER_AUTOLOGIN_EXPIRES_IN} will be |
| 880 | * used |
| 881 | * |
| 882 | */ |
| 883 | function user_cookieauth_create_cookie($id = NULL, $expires = NULL) { |
| 884 | global $user; |
| 885 | |
| 886 | if ($expires == NULL) { |
| 887 | log_debug('Automatic login token created for ' . $user['uid']); |
| 888 | } else { |
| 889 | log_debug('Automatic login token renewed for ' . $user['uid']); |
| 890 | } |
| 891 | |
| 892 | if ($id == NULL) $id = random_id(); |
| 893 | if ($expires == NULL) $expires = time() + SIMPLEID_USER_AUTOLOGIN_EXPIRES_IN; |
| 894 | $token = random_secret(); |
| 895 | $uid_hash = get_form_token($user['uid'], FALSE); |
| 896 | |
| 897 | $data = array( |
| 898 | 'uid' => $user['uid'], |
| 899 | 'token' => $token, |
| 900 | 'expires' => $expires, |
| 901 | 'uaid' => get_user_agent_id(), |
| 902 | 'ip' => $_SERVER['REMOTE_ADDR'] |
| 903 | ); |
| 904 | |
| 905 | cache_set('autologin-'. $uid_hash, $id, $data); |
| 906 | |
| 907 | // Note the last parameter (httponly) requires PHP 5.2 |
| 908 | setcookie(simpleid_cookie_name('auth'), 'cookieauth:' . $uid_hash . ':' . $id . ':' . $token, $expires, get_base_path(), '', false, true); |
| 909 | } |
| 910 | |
| 911 | /** |
| 912 | * Verifies a auto login cookie. If valid, log in the user automatically. |
| 913 | */ |
| 914 | function user_cookieauth_user_auto_login() { |
| 915 | if (!isset($_COOKIE[simpleid_cookie_name('auth')])) return NULL; |
| 916 | |
| 917 | $cookie = $_COOKIE[simpleid_cookie_name('auth')]; |
| 918 | |
| 919 | list($authtype, $uid_hash, $id, $token) = explode(':', $cookie); |
| 920 | if ($authtype != 'cookieauth') return NULL; |
| 921 | |
| 922 | log_debug('Automatic login token detected: ' . implode(':', ['cookieauth', $uid_hash, $id])); |
| 923 | |
| 924 | cache_expire(array('autologin-' . $uid_hash => SIMPLEID_USER_AUTOLOGIN_EXPIRES_IN)); |
| 925 | $data = cache_get('autologin-' . $uid_hash, $id); |
| 926 | |
| 927 | if (!$data) { // Cookie doesn't exist |
| 928 | log_notice('Automatic login: Token does not exist on server'); |
| 929 | return NULL; |
| 930 | } |
| 931 | |
| 932 | if ($data['expires'] < time()) { // Cookie expired |
| 933 | log_notice('Automatic login: Token on server expired'); |
| 934 | return NULL; |
| 935 | } |
| 936 | |
| 937 | if ($data['token'] != $token) { |
| 938 | log_warn('Automatic login: Token on server does not match'); |
| 939 | // Token not the same - panic |
| 940 | cache_expire(array('autologin-' . $uid_hash => 0)); |
| 941 | user_cookieauth_invalidate(); |
| 942 | return NULL; |
| 943 | } |
| 944 | |
| 945 | if ($data['uaid'] != get_user_agent_id()) { |
| 946 | log_warn('Automatic login: User agent ID does not match'); |
| 947 | // Token not the same - panic |
| 948 | cache_expire(array('autologin-' . $uid_hash => 0)); |
| 949 | user_cookieauth_invalidate(); |
| 950 | return NULL; |
| 951 | } |
| 952 | |
| 953 | // Load the user, tag it as an auto log in |
| 954 | $test_user = user_load($data['uid']); |
| 955 | |
| 956 | if ($test_user != NULL) { |
| 957 | log_debug('Automatic login token accepted for ' . $data['uid']); |
| 958 | |
| 959 | $test_user['autologin'] = TRUE; |
| 960 | |
| 961 | // Renew the token |
| 962 | user_cookieauth_create_cookie($id, $data['expires']); |
| 963 | |
| 964 | return $test_user; |
| 965 | } else { |
| 966 | log_warn('Automatic login token accepted for ' . $data['uid'] . ', but no such user exists'); |
| 967 | return NULL; |
| 968 | } |
| 969 | } |
| 970 | |
| 971 | /** |
| 972 | * Removes the auto login cookie from the user agent and the SimpleID |
| 973 | * cache. |
| 974 | */ |
| 975 | function user_cookieauth_invalidate() { |
| 976 | if (isset($_COOKIE[simpleid_cookie_name('auth')])) { |
| 977 | $cookie = $_COOKIE[simpleid_cookie_name('auth')]; |
| 978 | |
| 979 | list($uid_hash, $id, $token) = explode(':', $cookie); |
| 980 | |
| 981 | cache_delete('autologin-' . $uid_hash, $id); |
| 982 | |
| 983 | setcookie(simpleid_cookie_name('auth'), "", time() - 3600); |
| 984 | } |
| 985 | } |
| 986 | |
| 987 | /** |
| 988 | * Calculates a Time-Based One-Time Password (TOTP) based on RFC 6238. |
| 989 | * |
| 990 | * This function returns an integer calculated from the TOTP algorithm. |
| 991 | * The returned integer may need to be zero-padded to return a string |
| 992 | * with the required number of digits |
| 993 | * |
| 994 | * @param string $secret the shared secret as a binary string |
| 995 | * @param int $time the time to use in the HOTP algorithm. If NULL, the |
| 996 | * current time is used |
| 997 | * @param int $period the time step in seconds |
| 998 | * @param int $drift the number of time steps to be added to the time to |
| 999 | * adjust for transmission delay |
| 1000 | * @param string $algorithm the hashing algorithm as supported by |
| 1001 | * the hash_hmac() function |
| 1002 | * @param int $digits the number of digits in the one-time password |
| 1003 | * @return int the one-time password |
| 1004 | * @link http://tools.ietf.org/html/rfc6238 |
| 1005 | */ |
| 1006 | function user_totp($secret, $time = NULL, $period = 30, $drift = 0, $algorithm = 'sha1', $digits = 6) { |
| 1007 | if ($time == NULL) $time = time(); |
| 1008 | $counter = floor($time / $period) + $drift; |
| 1009 | $data = pack('NN', 0, $counter); |
| 1010 | return user_hotp($secret, $data, $algorithm, $digits); |
| 1011 | } |
| 1012 | |
| 1013 | /** |
| 1014 | * Calculates a HMAC-Based One-Time Password (HOTP) based on RFC 4226. |
| 1015 | * |
| 1016 | * This function returns an integer calculated from the HOTP algorithm. |
| 1017 | * The returned integer may need to be zero-padded to return a string |
| 1018 | * with the required number of digits |
| 1019 | * |
| 1020 | * @param string $secret the shared secret as a binary string |
| 1021 | * @param string $data the counter value as a 64 bit in |
| 1022 | * big endian encoding |
| 1023 | * @param string $algorithm the hashing algorithm as supported by |
| 1024 | * the hash_hmac() function |
| 1025 | * @param int $digits the number of digits in the one-time password |
| 1026 | * @return int the one-time password |
| 1027 | * @link http://tools.ietf.org/html/rfc4226 |
| 1028 | */ |
| 1029 | function user_hotp($secret, $data, $algorithm = 'sha1', $digits = 6) { |
| 1030 | // unpack produces a 1-based array, we use array_merge to convert it to 0-based |
| 1031 | $hmac = array_merge(unpack('C*', hash_hmac(strtolower($algorithm), $data, $secret, true))); |
| 1032 | $offset = $hmac[19] & 0xf; |
| 1033 | $code = ($hmac[$offset + 0] & 0x7F) << 24 | |
| 1034 | ($hmac[$offset + 1] & 0xFF) << 16 | |
| 1035 | ($hmac[$offset + 2] & 0xFF) << 8 | |
| 1036 | ($hmac[$offset + 3] & 0xFF); |
| 1037 | return $code % pow(10, $digits); |
| 1038 | } |
| 1039 | |
| 1040 | |
| 1041 | if (!function_exists('hash_pbkdf2') && function_exists('hash_hmac')) { |
| 1042 | function hash_pbkdf2($algo, $password, $salt, $iterations, $length = 0, $raw_output = false) { |
| 1043 | $result = ''; |
| 1044 | $hLen = strlen(hash($algo, '', true)); |
| 1045 | if ($length == 0) { |
| 1046 | $length = $hLen; |
| 1047 | if (!$raw_output) $length *= 2; |
| 1048 | } |
| 1049 | $l = ceil($length / $hLen); |
| 1050 | |
| 1051 | for ($i = 1; $i <= $l; $i++) { |
| 1052 | $U = hash_hmac($algo, $salt . pack('N', $i), $password, true); |
| 1053 | $T = $U; |
| 1054 | for ($j = 1; $j < $iterations; $j++) { |
| 1055 | $T ^= ($U = hash_hmac($algo, $U, $password, true)); |
| 1056 | } |
| 1057 | $result .= $T; |
| 1058 | } |
| 1059 | |
| 1060 | return substr(($raw_output) ? $result : bin2hex($result), 0, $length); |
| 1061 | } |
| 1062 | } |
| 1063 | |
| 1064 | |
| 1065 | ?> |