Code:
<?php
uselib('db/db_funcs_v1');
uselib('db/npflags_v1');
uselib('content/neofriend_func');
uselib('neopets/UserUtils');
uselib('neopets/flag_defs');
NeopetsSiteFlags::register();
class NPCookies {
const LOGIN_COOKIE_NAME = 'neologin';
const TOOLBAR_COOKIE_NAME = 'toolbar';
const SALT_NORMAL = '88f5065ac4';
const SALT_STEALTH = 'e044aa45d2';
/**
* Constrain the cookie code value to a legal range. The lower limit is
* 1000, not 1, because neologin_decode() returns 0 if the ciphertext is
* malformed. Since we allow cookie codes to match within +/- 2 of the
* actual value, this would cause a false authentication if someone's
* cookie_code was 1 or 2, and the cookie decrypted to 0.
*
* @param int $val
* @(you need an account to see links)
* @access public
* @(you need an account to see links) int
*/
public static function clamp_cookie_code($val) {
return min(max($val, 1000), 999999999);
}
/**
* Break the login cookie into its component parts. Returns false if the
* cookie is malformed.
*
* @(you need an account to see links)
* @access public
* @(you need an account to see links) array|bool
*/
public static function parse_login_cookie() {
$out = false;
if (!isset($_COOKIE[self::LOGIN_COOKIE_NAME])) return false;
if (preg_match("/^([^+]+)\+([^+]+)$/", $_COOKIE[self::LOGIN_COOKIE_NAME], $matches)) {
$out = array(
'cookie' => $_COOKIE[self::LOGIN_COOKIE_NAME],
'username' => strtolower(substr($matches[1], 0, 20)), // make sure it's max 20 chars long and is all lowercase
'ciphertext' => $matches[2]
);
}
return $out;
}
/**
* Break the toolbar cookie into its component parts. Returns false if the
* cookie is malformed.
*
* @(you need an account to see links)
* @access public
* @(you need an account to see links) array|bool
*/
public static function parse_toolbar_cookie() {
$out = false;
if (!isset($_COOKIE[self::TOOLBAR_COOKIE_NAME])) return false;
if (preg_match("/^([^+]+)\+([^+]+)\+([^+]+)$/", $_COOKIE[self::TOOLBAR_COOKIE_NAME], $matches)) {
$out = array(
'cookie' => $_COOKIE[self::TOOLBAR_COOKIE_NAME],
'username' => strtolower(substr($matches[1], 0, 20)), // make sure it's max 20 chars long and is all lowercase
'userclass' => $matches[2],
'ciphertext' => $matches[3]
);
}
return $out;
}
/**
* Return the standard formatted input for the cookie hash function to use.
*
* @param string $username
* @param string $password
* @(you need an account to see links)
* @access public
* @(you need an account to see links) string
*/
public static function get_normal_key($username, $password) {
return "{$username}{$password}" . self::SALT_NORMAL;
}
/**
* Same as get_normal_key(), but with the stealth salt.
*
* @param string $username
* @param string $password
* @(you need an account to see links)
* @access public
* @(you need an account to see links) string
*/
public static function get_stealth_key($username, $password) {
return "{$username}{$password}" . self::SALT_STEALTH;
}
/**
* Basic function for hashing a cookie value. $key is their password, and
* $plaintext is the cookie_code value.
*
* @param string $key
* @param string $plaintext
* @(you need an account to see links)
* @access public
* @(you need an account to see links) string
*/
public static function cookie_hash_encrypt($key, $plaintext) {
// Do not change the message string below or it'll log everyone out.
// Conversely, if you want to log everyone out, change the message string below. :)
$message = "npc_{$key}_{$plaintext}_biglongrandomstringhahalolthecakeisalie";
return sha1($message);
}
/**
* Set the login cookie.
*
* @param string $username
* @param string $key
* @param string $plaintext
* @param int $expire
* @(you need an account to see links)
* @access public
* @(you need an account to see links) void
*/
public static function set_login_cookie($username, $key, $plaintext, $expire) {
// make sure the cookie is never set for frozen users - NEADMN-34 - adamb / 2010-06-24
if (is_frozen($username)) {
return false;
}
$cookie_value = "{$username}+" . self::cookie_hash_encrypt($key, $plaintext);
setcookie(self::LOGIN_COOKIE_NAME, $cookie_value, $expire, "/", ".neopets.com");
$_COOKIE[self::LOGIN_COOKIE_NAME] = $cookie_value;
}
/**
* Set the toolbar cookie. This is used for the VSI/premium toolbar.
*
* @param string $username
* @param string $key
* @param string $plaintext
* @param int $expire
* @param string $password
* @param string $dob
* @(you need an account to see links)
* @access public
* @(you need an account to see links) void
*/
public static function set_toolbar_cookie($username, $key, $plaintext, $expire, $password, $dob) {
// make sure the cookie is never set for frozen users - NEADMN-34 - adamb / 2010-06-24
if (is_frozen($username)) {
return false;
}
$hash = self::cookie_hash_encrypt($key, $plaintext);
$permission = npflag_check($username, NPFLAG_PARENTAL_PERMISSION);
$agecode = self::toolbar_cookie_agecode($dob, $permission);
$cookie_value = "{$username}+{$agecode}+{$hash}";
setcookie(self::TOOLBAR_COOKIE_NAME, $cookie_value, $expire, "/", ".neopets.com");
$_COOKIE[self::TOOLBAR_COOKIE_NAME] = $cookie_value;
}
/**
* Calculate the agecode to put into the toolbar cookie.
*
* @param string $dob
* @param bool $permission
* @(you need an account to see links)
* @access public
* @(you need an account to see links) string
*/
public static function toolbar_cookie_agecode($dob, $permission) {
if ($dob == '') $age = 0;
else $age = UserUtils::ageFromDob($dob);
if ($age < 13) {
if (!$permission) {
$agecode = 'A';
} else {
$agecode = 'D';
}
} else if ($age < 18) {
$agecode = 'B';
} else {
$agecode = 'C';
}
return $agecode;
}
/**
* Update last_logged_time and cookie_code.
*
* @param string $username
* @param int $time
* @param string $cookie_code
* @(you need an account to see links)
* @access public
* @(you need an account to see links) void
*/
public static function update_llt_cc($username, $time, $cookie_code) {
$sql = "UPDATE personal SET last_logged_time = '{$time}', cookie_code = '{$cookie_code}' WHERE username = '{$username}'";
$rsUpdate = myoci("", $sql);
return $rsUpdate;
}
/**
* Try to decode a cookie with both the regular and stealth salts, and
* return which one worked (if any).
*
* @param string $username
* @param string $password
* @param int $cookie_code
* @(you need an account to see links)
* @access public
* @(you need an account to see links) array
*/
public static function decrypt_login_cookie($username, $password, $cookie_code) {
$parts = self::parse_login_cookie();
if ($parts == false) return false;
$out = array(
'parts' => $parts,
'normal' => false,
'stealth' => false,
'pt_normal' => '',
'pt_stealth' => '',
);
$key_normal = self::get_normal_key($username, $password);
$key_stealth = self::get_stealth_key($username, $password);
// The order here is from most likely to least likely. Usually the 0 offset will match;
// if not, then it's likely that they're 1 behind. If not, then 2 behind. Failing that,
// we try +1 and +2 which will almost never succeed.
$offsets = array(0, -1, -2, 1, 2);
foreach ($offsets as $offset) {
// For each offset, we need to try encrypting with the cookie_code plus that offset and see if it matches.
$normal_hash = self::cookie_hash_encrypt($key_normal, $cookie_code + $offset);
if ($normal_hash == $parts['ciphertext']) $out['normal'] = true;
else {
$stealth_hash = self::cookie_hash_encrypt($key_stealth, $cookie_code + $offset);
if ($stealth_hash == $parts['ciphertext']) $out['stealth'] = true;
}
// If we found the correct hash, there's no reason to check the rest.
if ($out['normal'] == true || $out['stealth'] == true) break;
}
return $out;
}
/**
* Same as decrypt_login_cookie, but for the toolbar cookie.
*
* @param string $username
* @param string $password
* @param int $cookie_code
* @(you need an account to see links)
* @access public
* @(you need an account to see links) array
*/
public static function decrypt_toolbar_cookie($username, $password, $cookie_code) {
$parts = self::parse_toolbar_cookie();
if ($parts == false) return false;
$out = array(
'parts' => $parts,
'normal' => false,
'stealth' => false,
'pt_normal' => '',
'pt_stealth' => '',
);
$key_normal = self::get_normal_key($username, $password);
$key_stealth = self::get_stealth_key($username, $password);
// The order here is from most likely to least likely. Usually the 0 offset will match;
// if not, then it's likely that they're 1 behind. If not, then 2 behind. Failing that,
// we try +1 and +2 which will almost never succeed.
$offsets = array(0, -1, -2, 1, 2);
// Try to decrypt it the old way. If it works, yay. If not, try the new way.
// Once there are no more cookies encrypted the old way, we can get rid of this.
// For the toolbar cookie, the "old way" was a simple MD5 hash of the password. Clever.
if ($parts['ciphertext'] == md5($password)) {
$out['normal'] = true;
return $out;
} else {
// Okay, it didn't work using the old method. Try the new method.
foreach ($offsets as $offset) {
// For each offset, we need to try encrypting with the cookie_code plus that offset and see if it matches.
$normal_hash = self::cookie_hash_encrypt($key_normal, $cookie_code + $offset);
if ($normal_hash == $parts['ciphertext']) $out['normal'] = true;
else {
$stealth_hash = self::cookie_hash_encrypt($key_stealth, $cookie_code + $offset);
if ($stealth_hash == $parts['ciphertext']) $out['stealth'] = true;
}
// If we found the correct hash, there's no reason to check the rest.
if ($out['normal'] == true || $out['stealth'] == true) break;
}
}
return $out;
}
/**
* The main cookie manipulation method. Can be used to set, update, check,
* or clear the login cookie.
*
* @param obj $NPUser
* @param string $action
* @param string $lang
* @(you need an account to see links)
* @access public
* @(you need an account to see links) bool
*/
public static function update_login_cookie($NPUser, $action, $lang) {
// Convenience vars
$new_code = intval($NPUser->cookie_code);
// Make sure cookie_code is in the valid range.
$new_code = self::clamp_cookie_code($new_code);
$now = time();
// set cookie expiration time, admins only get 12 hours
$expire = ($NPUser->is_admin) ? ($now + (3600 * 12)) : ($now + (3600 * 24 * 365));
if ($action == 'login' || $action == 'stealth') {
if ($action == 'login') {
// Anyone whose password isn't a 20-char hex string gets it changed forcibly here.
// This block can be deleted on or after 2012-03-01. - waggonem 2012-01-11
if (!preg_match('/^[a-f0-9]{20}$/', $NPUser->password)) {
$token = self::resetLoginToken($NPUser->username);
$NPUser->password = $token;
}
$key = self::get_normal_key($NPUser->username, $NPUser->password);
// Regular logins cause the last_logged_time and cookie_code to be updated;
// stealth logins do not.
$new_code = self::clamp_cookie_code($new_code + 1);
self::update_llt_cc($NPUser->username, $now, $new_code);
// Also do session tracking. TODO: Shouldn't this use $now?
self::session_tracker($NPUser, $lang);
} else if ($action == 'stealth') {
$key = self::get_stealth_key($NPUser->username, $NPUser->password);
}
// Set the login cookie.
self::set_login_cookie($NPUser->username, $key, $new_code, $expire);
// Set the old login cookie, too.
self::set_toolbar_cookie($NPUser->username, $key, $new_code, $expire, $NPUser->password, $NPUser->dob);
} else if ($action == 'logout') {
// Log out. This causes all the login cookies to be cleared from the
// user's computer. If the user is currently normally logged in, then
// we also update their llt and cookie_code, which will invalidate any
// OTHER cookies (that, e.g. might have gotten grabbed).
// Stealth logouts do not cause an llt/cc update, because we don't want
// to interfere with the legitimately logged-in user.
$decrypt = self::decrypt_login_cookie($NPUser->username, $NPUser->password, $new_code);
if ($decrypt != false && $decrypt['normal'] == true) {
$new_code = self::clamp_cookie_code($new_code + 5);
self::update_llt_cc($NPUser->username, $now, $new_code);
// Track session info.
self::session_tracker($NPUser, $lang);
}
// Clear all the login cookies.
setcookie(self::LOGIN_COOKIE_NAME, '', 0, "/", ".neopets.com");
setcookie(self::TOOLBAR_COOKIE_NAME, '', 0, "/", ".neopets.com");
} else if ($action == 'check' || $action == 'check_no_update') {
// Validate the cookie. This is used to determine whether the user is actually logged in.
// The 'check_no_update' action means that we only examine the cookie, we don't even bother
// trying to update it.
$decrypt = self::decrypt_login_cookie($NPUser->username, $NPUser->password, $new_code);
// The cookie was malformed, or the ciphertext did not decrypt with either key.
if ($decrypt == false || ($decrypt['normal'] == false && $decrypt['stealth'] == false)) {
return false;
}
// If it's been more than 5 minutes since the last update, we want to
// increment the cookie_code counter and resave the cookie.
if ($action == 'check' && $now - $NPUser->last_logged_time > 300) {
// If it's a normal login, then we update llt and cookie_code.
if ($decrypt['normal'] == true) {
// Update their last_logged_time and cookie_code.
$new_code = self::clamp_cookie_code($new_code + 1);
self::update_llt_cc($NPUser->username, $now, $new_code);
// track session info
self::session_tracker($NPUser, $lang);
$key = self::get_normal_key($NPUser->username, $NPUser->password);
} else if ($decrypt['stealth'] == true) {
$key = self::get_stealth_key($NPUser->username, $NPUser->password);
}
// Refresh the cookie for normal AND stealthed users.
self::set_login_cookie($NPUser->username, $key, $new_code, $expire);
// Refresh the old cookie, too.
self::set_toolbar_cookie($NPUser->username, $key, $new_code, $expire, $NPUser->password, $NPUser->dob);
}
// Whether or not we updated llt/cc and refreshed the cookie, either the old one
// or the new one matched. Validated! Yay!
//return true;
// return false if the user is frozen - NEADMN-34 - adamb / 2010-06-24
return is_frozen($NPUser->username) ? false : true;
}
// Return false in all other cases.
return false;
}
/**
* Track user sessions. A horrible abomination.
*
* @param obj $NPUser
* @param string $lang
* @(you need an account to see links)
* @access public
* @(you need an account to see links) void
*/
public static function session_tracker($NPUser, $lang) {
// This is horrible. We have to stop using this somehow. But who can
// save us?
global $xt6Yr4e33D;
if ($xt6Yr4e33D != '') {
$code = $xt6Yr4e33D;
$now = time();
$cutoff = $now - 1800;
if ($NPUser->last_logged_time < $cutoff) {
$sql = "INSERT IGNORE INTO expired_sessions SELECT * FROM active_sessions WHERE code = '{$code}' AND end_time < '{$cutoff}'";
myoci('', $sql);
$sql = "INSERT INTO active_sessions (code, start_time, end_time, ip, lang) VALUES ('{$code}', '{$now}', '{$now}', '{$_SERVER['REMOTE_ADDR']}', '{$lang}') ON DUPLICATE KEY UPDATE end_time = '{$now}', start_time = '{$now}', ip = '{$_SERVER['REMOTE_ADDR']}', lang = '{$lang}'";
myoci('', $sql);
} else {
$sql = "INSERT INTO active_sessions (code, start_time, end_time, ip, lang) VALUES ('{$code}', '{$now}', '{$now}', '{$_SERVER['REMOTE_ADDR']}', '{$lang}') ON DUPLICATE KEY UPDATE end_time = '{$now}'";
myoci('', $sql);
}
if ($NPUser->username != '') {
$sql = "UPDATE active_sessions SET username = '{$NPUser->username}', userflag = '{$NPUser->flags}', gender = '{$NPUser->sex}', dob = '{$NPUser->dob}', age = '{$NPUser->age}', dma = '{$NPUser->dma}', lang = '{$lang}', signup_country = '{$NPUser->country}', ip_country = '{$NPUser->geoip_country}', zip = '{$NPUser->zipcode}' WHERE code = '{$code}'";
myoci('', $sql);
}
}
}
/**
* We need to be able to generate a random 20-char string to put into the
* password field, now that it is no longer containing the user's canonical
* password.
*/
public static function genPasswordToken() {
mt_srand();
return substr(sha1(uniqid('', true)), 0, 20);
}
/**
* Reset a user's login token.
*/
public static function resetLoginToken($username) {
$token = self::genPasswordToken();
$sql = "UPDATE personal SET password = '{$token}' WHERE username = '{$username}' LIMIT 1";
$rsUpdate = myoci('', $sql);
return $token;
}
}