956 lines
24 KiB
PHP
956 lines
24 KiB
PHP
<?php
|
|
|
|
final class UserPwd {
|
|
// 32 bytes as hex
|
|
const HMAC_SECRET = '1e1e4476a89b28307dd39a56df4223bad7fea2045283a65ac3092a7adbf7f546';
|
|
|
|
// xor key, 128 bytes as hex, must be longer than the encrypted data
|
|
const XOR_KEY = 'fc2417fb8df1d82889f42a40a6241ea9b29de83ed7bf58d0c3d26e39e2bec70af0b2b9451c4a42e79a02dc6b78f614bfa4b5a89657215f46e293858f8fb8959d6e2b2fc40ce26ab25c25bfca21deeaed70318d33f734cba5c92870aa42fa70145a11b44329e694f0eada34b2fa7e3f8bf78444bf7c28002f27a23ff40eb2c253';
|
|
|
|
// size of the random nonce for xor encryption
|
|
const NONCE_SIZE = 12;
|
|
|
|
// Password length in bytes
|
|
const PWD_SIZE = 16;
|
|
// Length of non-pwd data in bytes (without sigs)
|
|
const DATA_SIZE = 31;
|
|
// Signature size in bytes
|
|
const SIG_SIZE = 4;
|
|
// Number of signatures
|
|
const SIG_COUNT = 4;
|
|
|
|
const MAX_B64_SIZE = 256;
|
|
|
|
// Timestamps will be reset if idle time is longer than TTL
|
|
const TTL = 604800; // 7 days
|
|
|
|
// Action counts can only be incremented once every ACTION_DELAY seconds
|
|
const ACTION_DELAY = 14400; // 4 hours
|
|
|
|
// If the IP changes before this delay, the ip_change_score will be increased
|
|
const IP_CHANGE_DELAY = 1800; // 30 minutes
|
|
const IP_CHANGE_SCORE_MAX = 32;
|
|
const IP_CHANGE_MASK_VAL = 3;
|
|
const IP_CHANGE_IP_VAL = 1;
|
|
|
|
const COOKIE_NAME = '4chan_pass';
|
|
|
|
const COOKIE_TTL = 31536000; // 1 year
|
|
|
|
const VERSION = 3;
|
|
const VERSION_MIN = 2;
|
|
|
|
const A_POST = 1;
|
|
const A_IMG = 2;
|
|
const A_THREAD = 4;
|
|
const A_REPORT = 8;
|
|
|
|
// password (raw)
|
|
private $pwd_raw = null;
|
|
// password (hex)
|
|
private $pwd_hex = null;
|
|
// password creation timestamp
|
|
private $creation_ts = 0;
|
|
// masked IP timestamp
|
|
private $mask_ts = 0;
|
|
// IP timestamp
|
|
private $ip_ts = 0;
|
|
// last activity timestamp
|
|
private $activity_ts = 0;
|
|
// last time action count was incremented
|
|
private $action_ts = 0;
|
|
// last time the environment changed (browser, country, etc)
|
|
private $env_ts = 0;
|
|
|
|
// Lvel of verification (currently unused)
|
|
private $verified_level = 0;
|
|
|
|
// Numbers of posts, image posts, threads and reports made (unsigned char)
|
|
private $post_count = 0;
|
|
private $img_count = 0;
|
|
private $thread_count = 0;
|
|
private $report_count = 0;
|
|
|
|
private $action_buffer = 0;
|
|
|
|
// If a an IP changes too soon, the score increases.
|
|
// IP changes increase the value by 2
|
|
// Mask changes increase the value by 4
|
|
// Stable activity reduces the score by 1
|
|
private $ip_change_score = 0; // unsigned char, 0-32
|
|
|
|
// hmac hash for pwd: pwd + creation_ts + activity_ts + action_ts + counts + domain
|
|
private $pwd_sig = null;
|
|
// hmac hash for masked IP: pwd + mask_ts + masked_ip + domain
|
|
private $mask_sig = null;
|
|
// hmac hash for IP: pwd + ip_ts + ip + domain
|
|
private $ip_sig = null;
|
|
// hmac hash for environment: pwd + env_ts + env + domain
|
|
private $env_sig = null;
|
|
|
|
private $env_data = null;
|
|
|
|
private $ip = null;
|
|
private $domain = null;
|
|
|
|
private $now = 0;
|
|
|
|
public $errno = 0;
|
|
|
|
private $version = 0;
|
|
|
|
private static $session_instance = null;
|
|
|
|
const
|
|
E_CORRUPT_LEN = 1,
|
|
E_CORRUPT_DEC = 2,
|
|
E_ENC = 11,
|
|
E_EXPIRED = 12,
|
|
E_PWDSIG = 13,
|
|
E_MASKSIG = 14,
|
|
E_IPSIG = 15,
|
|
E_ENVSIG = 16,
|
|
E_VERSION = 99
|
|
;
|
|
|
|
// Extract and return the hex pwd from a base64 string
|
|
public static function decodePwd($b64_data) {
|
|
if (!$b64_data || strlen($b64_data) > 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);
|
|
}
|
|
}
|