self::MAX_B64_SIZE) { return null; } $bin_data = self::b64_decode($b64_data); if (!$bin_data) { return null; } $version = unpack('C', $bin_data)[1]; if (strlen($bin_data) < self::PWD_SIZE + 1) { return null; } $nonce = substr($bin_data, 1, self::NONCE_SIZE); $bin_data = substr($bin_data, 1 + self::NONCE_SIZE); $bin_data = self::decrypt($bin_data, $nonce); $pwd = substr($bin_data, 0, self::PWD_SIZE); if (!$pwd || strlen($pwd) !== self::PWD_SIZE) { return false; } return bin2hex($pwd); } public function version() { return $this->version; } public static function getSession() { return self::$session_instance; } public static function clearSession() { self::$session_instance = null; } private static function b64_encode($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } private static function b64_decode($data) { return base64_decode(strtr($data, '-_', '+/')); } // $b64_data is the url-safe version, see b64_encode method. public function __construct($ip, $domain, $b64_data = null, $start_session = true) { $this->now = time(); if ($start_session) { self::$session_instance = $this; } $this->ip = $ip; $this->domain = $domain; $this->env_data = $this->collect_env_data(); if ($b64_data === null) { $this->generate(); return; } if (strlen($b64_data) > self::MAX_B64_SIZE) { $this->errno = self::E_CORRUPT_LEN; $this->generate(); return; } $bin_data = self::b64_decode($b64_data); if (!$bin_data) { $this->errno = self::E_CORRUPT_DEC; $this->generate(); return; } $version = unpack('C', $bin_data)[1]; $this->version = $version; // Version check if ($version > self::VERSION || $version < self::VERSION_MIN) { $this->errno = self::E_VERSION; $this->generate(); return; } $nonce = substr($bin_data, 1, self::NONCE_SIZE); $bin_data = substr($bin_data, 1 + self::NONCE_SIZE); $bin_data = self::decrypt($bin_data, $nonce); if (!$bin_data) { $this->errno = self::E_ENC; $this->generate(); return; } // FIXME: Version 2 if ($version === 2) { $_data_size = self::DATA_SIZE - 1 - 4 - 1; $full_size = self::PWD_SIZE + $_data_size + (self::SIG_SIZE * (self::SIG_COUNT - 1)); if (strlen($bin_data) !== $full_size) { $this->errno = self::E_CORRUPT_LEN2; $this->generate(); return; } $pwd_raw = substr($bin_data, 0, self::PWD_SIZE); list($creation_ts, $mask_ts, $ip_ts, $activity_ts, $action_ts, $post_count, $img_count, $thread_count, $report_count, $ip_change_score) = array_values(unpack('V5t/C5a', substr($bin_data, self::PWD_SIZE, $_data_size))); $env_ts = $this->now; $verified_level = 0; $action_buffer = 0; $sig_start = self::PWD_SIZE + $_data_size; $pwd_sig = substr($bin_data, $sig_start, self::SIG_SIZE); $mask_sig = substr($bin_data, $sig_start + self::SIG_SIZE, self::SIG_SIZE); $ip_sig = substr($bin_data, $sig_start + self::SIG_SIZE * 2, self::SIG_SIZE); $env_sig = null; // Password signature $valid_pwd_sig = $this->calc_sig( [ $pwd_raw, $creation_ts, $activity_ts, $action_ts, $post_count, $img_count, $thread_count, $report_count, $ip_change_score, $domain ] ); } // Current version else { $full_size = self::PWD_SIZE + self::DATA_SIZE + (self::SIG_SIZE * self::SIG_COUNT); if (strlen($bin_data) !== $full_size) { $this->errno = self::E_CORRUPT_LEN; $this->generate(); return; } $pwd_raw = substr($bin_data, 0, self::PWD_SIZE); list($creation_ts, $mask_ts, $ip_ts, $activity_ts, $action_ts, $env_ts, $verified_level, $post_count, $img_count, $thread_count, $report_count, $action_buffer, $ip_change_score) = array_values(unpack('V6t/C7a', substr($bin_data, self::PWD_SIZE, self::DATA_SIZE))); $sig_start = self::PWD_SIZE + self::DATA_SIZE; $pwd_sig = substr($bin_data, $sig_start, self::SIG_SIZE); $mask_sig = substr($bin_data, $sig_start + self::SIG_SIZE, self::SIG_SIZE); $ip_sig = substr($bin_data, $sig_start + self::SIG_SIZE * 2, self::SIG_SIZE); $env_sig = substr($bin_data, $sig_start + self::SIG_SIZE * 3, self::SIG_SIZE); // Password signature $valid_pwd_sig = $this->calc_sig( [ $pwd_raw, $creation_ts, $activity_ts, $action_ts, $env_ts, $verified_level, $post_count, $img_count, $thread_count, $report_count, $action_buffer, $ip_change_score, $domain ] ); } if ($valid_pwd_sig && $valid_pwd_sig === $pwd_sig) { $this->pwd_raw = $pwd_raw; $this->pwd_hex = bin2hex($pwd_raw); if ($activity_ts > 0) { $_act_ts = $activity_ts; } else { $_act_ts = $creation_ts; } if ($this->now - $_act_ts >= self::TTL) { $this->errno = self::E_EXPIRED; $this->resetTimestamps(); return; } else { $this->creation_ts = $creation_ts; $this->activity_ts = $activity_ts; $this->action_ts = $action_ts; $this->env_ts = $env_ts; // FIXME: Version 2 if ($version !== 2) { $this->pwd_sig = $valid_pwd_sig; } $this->verified_level = $verified_level; $this->post_count = $post_count; $this->img_count = $img_count; $this->thread_count = $thread_count; $this->report_count = $report_count; $this->action_buffer = $action_buffer; $this->ip_change_score = $ip_change_score; } } else { $this->errno = self::E_PWDSIG; $this->generate(); return; } // Environment signature $valid_env_sig = $this->calc_sig([ $pwd_raw, $env_ts, $this->env_data, $domain ]); if ($valid_env_sig && $valid_env_sig === $env_sig) { $this->env_ts = $env_ts; $this->env_sig = $valid_env_sig; } else { $this->errno = self::E_ENVSIG; $this->env_ts = $this->now; $this->pwd_sig = null; // FIXME, env_ts shouldn't be used in the pwd_sig } // Masked IP signature $valid_mask_sig = $this->calc_sig([ $pwd_raw, $mask_ts, $this->get_ip_mask($ip), $domain ]); if ($valid_mask_sig && $valid_mask_sig === $mask_sig) { $this->mask_ts = $mask_ts; $this->mask_sig = $valid_mask_sig; } else { $this->mask_ts = $this->now; $this->ip_ts = $this->now; $this->errno = self::E_MASKSIG; return; // bail out } // IP signature $valid_ip_sig = $this->calc_sig([ $pwd_raw, $ip_ts, $ip, $domain ]); if ($valid_ip_sig && $valid_ip_sig === $ip_sig) { $this->ip_ts = $ip_ts; $this->ip_sig = $valid_ip_sig; } else { $this->errno = self::E_IPSIG; $this->ip_ts = $this->now; } } private function get_ip_mask($ip) { $ip_parts = explode('.', $ip, 3); return "{$ip_parts[0]}.{$ip_parts[1]}"; } private function collect_env_data() { if (!isset($_SERVER)) { return 'noenv'; } // Country if (isset($_SERVER['HTTP_X_GEO_COUNTRY'])) { $data = $_SERVER['HTTP_X_GEO_COUNTRY']; } else { $data = 'XX'; } return $data; } private function calc_sig($arg_array) { return substr(hash_hmac('sha1', implode(' ', $arg_array), UserPwd::HMAC_SECRET, true), 0, self::SIG_SIZE); } public function getPwd() { return $this->pwd_hex; } public function pwdLifetime() { if ($this->creation_ts) { return $this->now - $this->creation_ts; } else { return 0; } } public function maskLifetime() { if ($this->mask_ts) { return $this->now - $this->mask_ts; } else { return 0; } } public function ipLifetime() { if ($this->ip_ts) { return $this->now - $this->ip_ts; } else { return 0; } } public function envLifetime() { if ($this->env_ts) { return $this->now - $this->env_ts; } else { return 0; } } public function creationTs() { return $this->creation_ts; } public function ipTs() { return $this->ip_ts; } public function maskTs() { return $this->mask_ts; } public function idleLifetime() { if ($this->activity_ts) { return $this->now - $this->activity_ts; } else { return $this->creation_ts; } } public function lastActionLifetime() { if ($this->action_ts) { return $this->now - $this->action_ts; } else { return 0; } } public function verifiedLevel() { return $this->verified_level; } public function maskChanged() { return !$this->isNew() && $this->mask_ts === $this->now; } public function ipChanged() { return !$this->isNew() && $this->ip_ts === $this->now; } public function envChanged() { return !$this->isNew() && $this->env_ts === $this->now; } public function isUserKnown($for_minutes = 1440, $since_ts = 0) { // If the IP changes too often, enforce an IP lifetime of IP_CHANGE_DELAY if ($this->ipChangeScore() > self::IP_CHANGE_MASK_VAL * 3) { if ($this->maskLifetime() < self::IP_CHANGE_DELAY) { return false; } } // Mask is older than the required lifetime if ($this->maskLifetime() >= $for_minutes * 60) { return true; } // Mask was created before the reference time // ex: user was already posting when a new lenient rangeban was created if ($since_ts > 0 && $this->mask_ts <= $since_ts) { if ($this->postCount() > 0 || $this->reportCount() > 5) { return true; } } // Password isn't old enough if ($this->pwdLifetime() < $for_minutes * 60) { return false; } // Password is old enough // For lenient rangebans, this is enough if ($since_ts > 0) { return true; } // Otherwise, do some more checks // User has enough activity if ($this->postCount() >= 3 || $this->reportCount() >= 10) { // Check UA + country //if ($this->envLifetime() >= self::IP_CHANGE_DELAY) { // return true; //} // Check the mask lifetime if ($this->maskLifetime() >= self::IP_CHANGE_DELAY) { return true; } // Otherwise do a more strict activity check if ($this->postCount() >= 9 || $this->reportCount() >= 20) { return true; } } // All checks failed return false; } public function isUserKnownOrVerified($for_minutes = 1440, $since_ts = 0) { if ($this->verifiedLevel()) { return true; } return $this->isUserKnown($for_minutes, $since_ts); } public function updatePostActivity($is_thread, $has_file, $is_dummy = false) { $actions = self::A_POST; if ($is_thread) { $actions = $actions | self::A_THREAD; } if ($has_file) { $actions = $actions | self::A_IMG; } $this->updateActivity($actions, $is_dummy); } public function updateReportActivity($is_dummy = false) { $this->updateActivity(self::A_REPORT, $is_dummy); } public function updateActivity($kind, $is_dummy = false) { $this->action_buffer = $this->action_buffer | $kind; $ip_change_delta = -1; if ($this->idleLifetime() < self::IP_CHANGE_DELAY) { if ($this->maskChanged()) { $ip_change_delta = self::IP_CHANGE_MASK_VAL; } else if ($this->ipChanged()) { $ip_change_delta = self::IP_CHANGE_IP_VAL; } } $this->ip_change_score = min(max(0, $this->ip_change_score + $ip_change_delta), self::IP_CHANGE_SCORE_MAX); if ($this->ip_change_score >= self::IP_CHANGE_SCORE_MAX) { $this->resetActionCounts(); } if ($this->action_ts === 0) { $this->action_ts = $this->now; } else if (!$is_dummy && $this->lastActionLifetime() >= self::ACTION_DELAY) { if ($this->action_buffer & self::A_REPORT) { $this->report_count = min($this->report_count + 1, 0xFF); } if ($this->action_buffer & self::A_POST) { $this->post_count = min($this->post_count + 1, 0xFF); } if ($this->action_buffer & self::A_IMG) { $this->img_count = min($this->img_count + 1, 0xFF); } if ($this->action_buffer & self::A_THREAD) { $this->thread_count = min($this->thread_count + 1, 0xFF); } $this->action_buffer = 0; $this->action_ts = $this->now; } $this->activity_ts = $this->now; $this->pwd_sig = null; } public function postCount() { return $this->post_count + ($this->action_buffer & self::A_POST ? 1 : 0); } public function imgCount() { return $this->img_count + ($this->action_buffer & self::A_IMG ? 1 : 0); } public function threadCount() { return $this->thread_count + ($this->action_buffer & self::A_THREAD ? 1 : 0); } public function reportCount() { return $this->report_count + ($this->action_buffer & self::A_REPORT ? 1 : 0); } public function ipChangeScore() { return $this->ip_change_score; } // Never used public function isNeverUsed() { return $this->activity_ts === 0; } // Used only once public function isUsedOnlyOnce() { return $this->activity_ts === $this->creation_ts; } // Just created public function isNew() { return $this->creation_ts === $this->now; } // Fake or spoofed public function isFake() { return $this->errno === self::E_PWDSIG; } public function getEncodedData() { if (!$this->domain || !$this->ip) { return false; } $data = []; // Raw password if ($this->pwd_raw) { $data[] = $this->pwd_raw; } else { return false; } // Creation timestamp if ($this->creation_ts > 0) { $data[] = pack('V', $this->creation_ts); } else { return false; } // Mask timestamp if ($this->mask_ts > 0) { $data[] = pack('V', $this->mask_ts); } else { return false; } // IP timestamp if ($this->ip_ts > 0) { $data[] = pack('V', $this->ip_ts); } else { return false; } // Last ativity timestamp if ($this->activity_ts < 0) { return false; } $data[] = pack('V', $this->activity_ts); // Last action increment timestamp if ($this->action_ts < 0) { return false; } $data[] = pack('V', $this->action_ts); // Env timestamp if ($this->env_ts > 0) { $data[] = pack('V', $this->env_ts); } else { return false; } // Verified level if ($this->verified_level < 0) { return false; } $data[] = pack('C', $this->verified_level); // Action counts $data[] = pack('C5', $this->post_count, $this->img_count, $this->thread_count, $this->report_count, $this->action_buffer); // IP change score $data[] = pack('C', $this->ip_change_score); // Password signature if ($this->pwd_sig) { $data[] = $this->pwd_sig; } else { $data[] = $this->calc_sig([ $this->pwd_raw, $this->creation_ts, $this->activity_ts, $this->action_ts, $this->env_ts, $this->verified_level, $this->post_count, $this->img_count, $this->thread_count, $this->report_count, $this->action_buffer, $this->ip_change_score, $this->domain ]); } // Mask signature if ($this->mask_sig) { $data[] = $this->mask_sig; } else { $data[] = $this->calc_sig([ $this->pwd_raw, $this->mask_ts, $this->get_ip_mask($this->ip), $this->domain ]); } // IP signature if ($this->ip_sig) { $data[] = $this->ip_sig; } else { $data[] = $this->calc_sig([ $this->pwd_raw, $this->ip_ts, $this->ip, $this->domain ]); } // Env signature if ($this->env_sig) { $data[] = $this->env_sig; } else { $data[] = $this->calc_sig([ $this->pwd_raw, $this->env_ts, $this->env_data, $this->domain ]); } // --- $data = implode('', $data); list($data, $nonce) = self::encrypt($data); if (!$data) { return false; } // Version + Nonce $data = pack('C', self::VERSION) . $nonce . $data; return self::b64_encode($data); } private static function encrypt($data) { $data_len = strlen($data); $key = hex2bin(self::XOR_KEY); $nonce = openssl_random_pseudo_bytes(self::NONCE_SIZE); if (!$data_len || !$nonce || $data_len > strlen($key)) { return false; } $output_nonced = ''; // Apply nonce $ni = 0; for ($di = 0; $di < $data_len; ++$di) { if ($ni >= self::NONCE_SIZE) { $ni = 0; } $output_nonced = $output_nonced . ($data[$di] ^ $nonce[$ni]); $ni++; } $output = ''; // XOR Encrypt for ($i = 0; $i < $data_len; ++$i) { $output = $output . ($output_nonced[$i] ^ $key[$i]); } return [ $output, $nonce ]; } private static function decrypt($data, $nonce) { $data_len = strlen($data); $nonce_len = strlen($nonce); $key = hex2bin(self::XOR_KEY); if (!$data_len || !$nonce || $data_len > strlen($key)) { return false; } $output_nonced = ''; // XOR Decrypt for ($i = 0; $i < $data_len; ++$i) { $output_nonced = $output_nonced . ($data[$i] ^ $key[$i]); } // Apply nonce $output = ''; $ni = 0; for ($di = 0; $di < $data_len; ++$di) { if ($ni >= $nonce_len) { $ni = 0; } $output = $output . ($output_nonced[$di] ^ $nonce[$ni]); $ni++; } return $output; } private function generate() { if (!$this->ip || !$this->domain) { return false; } $pwd_raw = openssl_random_pseudo_bytes(self::PWD_SIZE); if (!$pwd_raw) { return false; } $this->version = self::VERSION; $this->pwd_raw = $pwd_raw; $this->pwd_hex = bin2hex($pwd_raw); $this->creation_ts = $this->now; $this->mask_ts = $this->now; $this->ip_ts = $this->now; $this->env_ts = $this->now; return true; } public function setPwd($pwd_hex) { if (!$pwd_hex) { return false; } $pwd_raw = hex2bin($pwd_hex); if (!$pwd_raw || strlen($pwd_raw) !== self::PWD_SIZE) { return false; } $this->pwd_raw = $pwd_raw; $this->pwd_hex = $pwd_hex; $this->resetSignatures(); return true; } public function setVerifiedLevel($level) { if ($level < 0) { return false; } $this->verified_level = $level; $this->pwd_sig = null; } private function resetTimestamps() { $this->creation_ts = $this->now; $this->mask_ts = $this->now; $this->ip_ts = $this->now; $this->action_ts = $this->now; $this->activity_ts = 0; $this->env_ts = $this->now; } private function resetActionCounts() { $this->post_count = 0; $this->img_count = 0; $this->thread_count = 0; $this->report_count = 0; $this->action_buffer = 0; } private function resetSignatures() { $this->pwd_sig = null; $this->mask_sig = null; $this->ip_sig = null; $this->env_sig = null; } public function setCookie($domain) { $data = $this->getEncodedData(); if ($data) { return setcookie(self::COOKIE_NAME, $data, $this->now + self::COOKIE_TTL, '/', $domain, true, true); } else { return false; } } public static function setFakeCookie($now, $domain) { $size = self::NONCE_SIZE + self::PWD_SIZE + self::DATA_SIZE + self::SIG_SIZE * self::SIG_COUNT; $data = openssl_random_pseudo_bytes($size); if (!$data) { return false; } $data = pack('C', self::VERSION) . $data; $data = self::b64_encode($data); return setcookie(self::COOKIE_NAME, $data, $now + self::COOKIE_TTL, '/', $domain, true); } }