userpwd.php
1 <?php 2 3 final class UserPwd { 4 // 32 bytes as hex 5 const HMAC_SECRET = '1e1e4476a89b28307dd39a56df4223bad7fea2045283a65ac3092a7adbf7f546'; 6 7 // xor key, 128 bytes as hex, must be longer than the encrypted data 8 const XOR_KEY = 'fc2417fb8df1d82889f42a40a6241ea9b29de83ed7bf58d0c3d26e39e2bec70af0b2b9451c4a42e79a02dc6b78f614bfa4b5a89657215f46e293858f8fb8959d6e2b2fc40ce26ab25c25bfca21deeaed70318d33f734cba5c92870aa42fa70145a11b44329e694f0eada34b2fa7e3f8bf78444bf7c28002f27a23ff40eb2c253'; 9 10 // size of the random nonce for xor encryption 11 const NONCE_SIZE = 12; 12 13 // Password length in bytes 14 const PWD_SIZE = 16; 15 // Length of non-pwd data in bytes (without sigs) 16 const DATA_SIZE = 31; 17 // Signature size in bytes 18 const SIG_SIZE = 4; 19 // Number of signatures 20 const SIG_COUNT = 4; 21 22 const MAX_B64_SIZE = 256; 23 24 // Timestamps will be reset if idle time is longer than TTL 25 const TTL = 604800; // 7 days 26 27 // Action counts can only be incremented once every ACTION_DELAY seconds 28 const ACTION_DELAY = 14400; // 4 hours 29 30 // If the IP changes before this delay, the ip_change_score will be increased 31 const IP_CHANGE_DELAY = 1800; // 30 minutes 32 const IP_CHANGE_SCORE_MAX = 32; 33 const IP_CHANGE_MASK_VAL = 3; 34 const IP_CHANGE_IP_VAL = 1; 35 36 const COOKIE_NAME = '4chan_pass'; 37 38 const COOKIE_TTL = 31536000; // 1 year 39 40 const VERSION = 3; 41 const VERSION_MIN = 2; 42 43 const A_POST = 1; 44 const A_IMG = 2; 45 const A_THREAD = 4; 46 const A_REPORT = 8; 47 48 // password (raw) 49 private $pwd_raw = null; 50 // password (hex) 51 private $pwd_hex = null; 52 // password creation timestamp 53 private $creation_ts = 0; 54 // masked IP timestamp 55 private $mask_ts = 0; 56 // IP timestamp 57 private $ip_ts = 0; 58 // last activity timestamp 59 private $activity_ts = 0; 60 // last time action count was incremented 61 private $action_ts = 0; 62 // last time the environment changed (browser, country, etc) 63 private $env_ts = 0; 64 65 // Lvel of verification (currently unused) 66 private $verified_level = 0; 67 68 // Numbers of posts, image posts, threads and reports made (unsigned char) 69 private $post_count = 0; 70 private $img_count = 0; 71 private $thread_count = 0; 72 private $report_count = 0; 73 74 private $action_buffer = 0; 75 76 // If a an IP changes too soon, the score increases. 77 // IP changes increase the value by 2 78 // Mask changes increase the value by 4 79 // Stable activity reduces the score by 1 80 private $ip_change_score = 0; // unsigned char, 0-32 81 82 // hmac hash for pwd: pwd + creation_ts + activity_ts + action_ts + counts + domain 83 private $pwd_sig = null; 84 // hmac hash for masked IP: pwd + mask_ts + masked_ip + domain 85 private $mask_sig = null; 86 // hmac hash for IP: pwd + ip_ts + ip + domain 87 private $ip_sig = null; 88 // hmac hash for environment: pwd + env_ts + env + domain 89 private $env_sig = null; 90 91 private $env_data = null; 92 93 private $ip = null; 94 private $domain = null; 95 96 private $now = 0; 97 98 public $errno = 0; 99 100 private $version = 0; 101 102 private static $session_instance = null; 103 104 const 105 E_CORRUPT_LEN = 1, 106 E_CORRUPT_DEC = 2, 107 E_ENC = 11, 108 E_EXPIRED = 12, 109 E_PWDSIG = 13, 110 E_MASKSIG = 14, 111 E_IPSIG = 15, 112 E_ENVSIG = 16, 113 E_VERSION = 99 114 ; 115 116 // Extract and return the hex pwd from a base64 string 117 public static function decodePwd($b64_data) { 118 if (!$b64_data || strlen($b64_data) > self::MAX_B64_SIZE) { 119 return null; 120 } 121 122 $bin_data = self::b64_decode($b64_data); 123 124 if (!$bin_data) { 125 return null; 126 } 127 128 $version = unpack('C', $bin_data)[1]; 129 130 if (strlen($bin_data) < self::PWD_SIZE + 1) { 131 return null; 132 } 133 134 $nonce = substr($bin_data, 1, self::NONCE_SIZE); 135 $bin_data = substr($bin_data, 1 + self::NONCE_SIZE); 136 $bin_data = self::decrypt($bin_data, $nonce); 137 $pwd = substr($bin_data, 0, self::PWD_SIZE); 138 139 if (!$pwd || strlen($pwd) !== self::PWD_SIZE) { 140 return false; 141 } 142 143 return bin2hex($pwd); 144 } 145 146 public function version() { 147 return $this->version; 148 } 149 150 public static function getSession() { 151 return self::$session_instance; 152 } 153 154 public static function clearSession() { 155 self::$session_instance = null; 156 } 157 158 private static function b64_encode($data) { 159 return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); 160 } 161 162 private static function b64_decode($data) { 163 return base64_decode(strtr($data, '-_', '+/')); 164 } 165 166 // $b64_data is the url-safe version, see b64_encode method. 167 public function __construct($ip, $domain, $b64_data = null, $start_session = true) { 168 $this->now = time(); 169 170 if ($start_session) { 171 self::$session_instance = $this; 172 } 173 174 $this->ip = $ip; 175 $this->domain = $domain; 176 177 $this->env_data = $this->collect_env_data(); 178 179 if ($b64_data === null) { 180 $this->generate(); 181 return; 182 } 183 184 if (strlen($b64_data) > self::MAX_B64_SIZE) { 185 $this->errno = self::E_CORRUPT_LEN; 186 $this->generate(); 187 return; 188 } 189 190 $bin_data = self::b64_decode($b64_data); 191 192 if (!$bin_data) { 193 $this->errno = self::E_CORRUPT_DEC; 194 $this->generate(); 195 return; 196 } 197 198 $version = unpack('C', $bin_data)[1]; 199 200 $this->version = $version; 201 202 // Version check 203 if ($version > self::VERSION || $version < self::VERSION_MIN) { 204 $this->errno = self::E_VERSION; 205 $this->generate(); 206 return; 207 } 208 209 $nonce = substr($bin_data, 1, self::NONCE_SIZE); 210 $bin_data = substr($bin_data, 1 + self::NONCE_SIZE); 211 $bin_data = self::decrypt($bin_data, $nonce); 212 213 if (!$bin_data) { 214 $this->errno = self::E_ENC; 215 $this->generate(); 216 return; 217 } 218 219 // FIXME: Version 2 220 if ($version === 2) { 221 $_data_size = self::DATA_SIZE - 1 - 4 - 1; 222 223 $full_size = self::PWD_SIZE + $_data_size + (self::SIG_SIZE * (self::SIG_COUNT - 1)); 224 225 if (strlen($bin_data) !== $full_size) { 226 $this->errno = self::E_CORRUPT_LEN2; 227 $this->generate(); 228 return; 229 } 230 231 $pwd_raw = substr($bin_data, 0, self::PWD_SIZE); 232 233 list($creation_ts, $mask_ts, $ip_ts, $activity_ts, $action_ts, 234 $post_count, $img_count, $thread_count, $report_count, $ip_change_score) 235 = array_values(unpack('V5t/C5a', substr($bin_data, self::PWD_SIZE, $_data_size))); 236 237 $env_ts = $this->now; 238 $verified_level = 0; 239 $action_buffer = 0; 240 241 $sig_start = self::PWD_SIZE + $_data_size; 242 243 $pwd_sig = substr($bin_data, $sig_start, self::SIG_SIZE); 244 $mask_sig = substr($bin_data, $sig_start + self::SIG_SIZE, self::SIG_SIZE); 245 $ip_sig = substr($bin_data, $sig_start + self::SIG_SIZE * 2, self::SIG_SIZE); 246 $env_sig = null; 247 248 // Password signature 249 $valid_pwd_sig = $this->calc_sig( 250 [ 251 $pwd_raw, $creation_ts, $activity_ts, $action_ts, 252 $post_count, $img_count, $thread_count, $report_count, $ip_change_score, 253 $domain 254 ] 255 ); 256 } 257 // Current version 258 else { 259 $full_size = self::PWD_SIZE + self::DATA_SIZE + (self::SIG_SIZE * self::SIG_COUNT); 260 261 if (strlen($bin_data) !== $full_size) { 262 $this->errno = self::E_CORRUPT_LEN; 263 $this->generate(); 264 return; 265 } 266 267 $pwd_raw = substr($bin_data, 0, self::PWD_SIZE); 268 list($creation_ts, $mask_ts, $ip_ts, $activity_ts, $action_ts, $env_ts, $verified_level, 269 $post_count, $img_count, $thread_count, $report_count, $action_buffer, $ip_change_score) 270 = array_values(unpack('V6t/C7a', substr($bin_data, self::PWD_SIZE, self::DATA_SIZE))); 271 272 $sig_start = self::PWD_SIZE + self::DATA_SIZE; 273 274 $pwd_sig = substr($bin_data, $sig_start, self::SIG_SIZE); 275 $mask_sig = substr($bin_data, $sig_start + self::SIG_SIZE, self::SIG_SIZE); 276 $ip_sig = substr($bin_data, $sig_start + self::SIG_SIZE * 2, self::SIG_SIZE); 277 $env_sig = substr($bin_data, $sig_start + self::SIG_SIZE * 3, self::SIG_SIZE); 278 279 // Password signature 280 $valid_pwd_sig = $this->calc_sig( 281 [ 282 $pwd_raw, $creation_ts, $activity_ts, $action_ts, $env_ts, $verified_level, 283 $post_count, $img_count, $thread_count, $report_count, $action_buffer, $ip_change_score, 284 $domain 285 ] 286 ); 287 } 288 289 if ($valid_pwd_sig && $valid_pwd_sig === $pwd_sig) { 290 $this->pwd_raw = $pwd_raw; 291 292 $this->pwd_hex = bin2hex($pwd_raw); 293 294 if ($activity_ts > 0) { 295 $_act_ts = $activity_ts; 296 } 297 else { 298 $_act_ts = $creation_ts; 299 } 300 301 if ($this->now - $_act_ts >= self::TTL) { 302 $this->errno = self::E_EXPIRED; 303 $this->resetTimestamps(); 304 return; 305 } 306 else { 307 $this->creation_ts = $creation_ts; 308 $this->activity_ts = $activity_ts; 309 $this->action_ts = $action_ts; 310 $this->env_ts = $env_ts; 311 312 // FIXME: Version 2 313 if ($version !== 2) { 314 $this->pwd_sig = $valid_pwd_sig; 315 } 316 317 $this->verified_level = $verified_level; 318 319 $this->post_count = $post_count; 320 $this->img_count = $img_count; 321 $this->thread_count = $thread_count; 322 $this->report_count = $report_count; 323 $this->action_buffer = $action_buffer; 324 325 $this->ip_change_score = $ip_change_score; 326 } 327 } 328 else { 329 $this->errno = self::E_PWDSIG; 330 $this->generate(); 331 return; 332 } 333 334 // Environment signature 335 $valid_env_sig = $this->calc_sig([ $pwd_raw, $env_ts, $this->env_data, $domain ]); 336 337 if ($valid_env_sig && $valid_env_sig === $env_sig) { 338 $this->env_ts = $env_ts; 339 $this->env_sig = $valid_env_sig; 340 } 341 else { 342 $this->errno = self::E_ENVSIG; 343 $this->env_ts = $this->now; 344 $this->pwd_sig = null; // FIXME, env_ts shouldn't be used in the pwd_sig 345 } 346 347 // Masked IP signature 348 $valid_mask_sig = $this->calc_sig([ $pwd_raw, $mask_ts, $this->get_ip_mask($ip), $domain ]); 349 350 if ($valid_mask_sig && $valid_mask_sig === $mask_sig) { 351 $this->mask_ts = $mask_ts; 352 $this->mask_sig = $valid_mask_sig; 353 } 354 else { 355 $this->mask_ts = $this->now; 356 $this->ip_ts = $this->now; 357 358 $this->errno = self::E_MASKSIG; 359 360 return; // bail out 361 } 362 363 // IP signature 364 $valid_ip_sig = $this->calc_sig([ $pwd_raw, $ip_ts, $ip, $domain ]); 365 366 if ($valid_ip_sig && $valid_ip_sig === $ip_sig) { 367 $this->ip_ts = $ip_ts; 368 $this->ip_sig = $valid_ip_sig; 369 } 370 else { 371 $this->errno = self::E_IPSIG; 372 $this->ip_ts = $this->now; 373 } 374 } 375 376 private function get_ip_mask($ip) { 377 $ip_parts = explode('.', $ip, 3); 378 return "{$ip_parts[0]}.{$ip_parts[1]}"; 379 } 380 381 private function collect_env_data() { 382 if (!isset($_SERVER)) { 383 return 'noenv'; 384 } 385 386 // Country 387 if (isset($_SERVER['HTTP_X_GEO_COUNTRY'])) { 388 $data = $_SERVER['HTTP_X_GEO_COUNTRY']; 389 } 390 else { 391 $data = 'XX'; 392 } 393 394 return $data; 395 } 396 397 private function calc_sig($arg_array) { 398 return substr(hash_hmac('sha1', implode(' ', $arg_array), UserPwd::HMAC_SECRET, true), 0, self::SIG_SIZE); 399 } 400 401 public function getPwd() { 402 return $this->pwd_hex; 403 } 404 405 public function pwdLifetime() { 406 if ($this->creation_ts) { 407 return $this->now - $this->creation_ts; 408 } 409 else { 410 return 0; 411 } 412 } 413 414 public function maskLifetime() { 415 if ($this->mask_ts) { 416 return $this->now - $this->mask_ts; 417 } 418 else { 419 return 0; 420 } 421 } 422 423 public function ipLifetime() { 424 if ($this->ip_ts) { 425 return $this->now - $this->ip_ts; 426 } 427 else { 428 return 0; 429 } 430 } 431 432 public function envLifetime() { 433 if ($this->env_ts) { 434 return $this->now - $this->env_ts; 435 } 436 else { 437 return 0; 438 } 439 } 440 441 public function creationTs() { 442 return $this->creation_ts; 443 } 444 445 public function ipTs() { 446 return $this->ip_ts; 447 } 448 449 public function maskTs() { 450 return $this->mask_ts; 451 } 452 453 public function idleLifetime() { 454 if ($this->activity_ts) { 455 return $this->now - $this->activity_ts; 456 } 457 else { 458 return $this->creation_ts; 459 } 460 } 461 462 public function lastActionLifetime() { 463 if ($this->action_ts) { 464 return $this->now - $this->action_ts; 465 } 466 else { 467 return 0; 468 } 469 } 470 471 public function verifiedLevel() { 472 return $this->verified_level; 473 } 474 475 public function maskChanged() { 476 return !$this->isNew() && $this->mask_ts === $this->now; 477 } 478 479 public function ipChanged() { 480 return !$this->isNew() && $this->ip_ts === $this->now; 481 } 482 483 public function envChanged() { 484 return !$this->isNew() && $this->env_ts === $this->now; 485 } 486 487 public function isUserKnown($for_minutes = 1440, $since_ts = 0) { 488 // If the IP changes too often, enforce an IP lifetime of IP_CHANGE_DELAY 489 if ($this->ipChangeScore() > self::IP_CHANGE_MASK_VAL * 3) { 490 if ($this->maskLifetime() < self::IP_CHANGE_DELAY) { 491 return false; 492 } 493 } 494 495 // Mask is older than the required lifetime 496 if ($this->maskLifetime() >= $for_minutes * 60) { 497 return true; 498 } 499 500 // Mask was created before the reference time 501 // ex: user was already posting when a new lenient rangeban was created 502 if ($since_ts > 0 && $this->mask_ts <= $since_ts) { 503 if ($this->postCount() > 0 || $this->reportCount() > 5) { 504 return true; 505 } 506 } 507 508 // Password isn't old enough 509 if ($this->pwdLifetime() < $for_minutes * 60) { 510 return false; 511 } 512 513 // Password is old enough 514 515 // For lenient rangebans, this is enough 516 if ($since_ts > 0) { 517 return true; 518 } 519 520 // Otherwise, do some more checks 521 522 // User has enough activity 523 if ($this->postCount() >= 3 || $this->reportCount() >= 10) { 524 // Check UA + country 525 //if ($this->envLifetime() >= self::IP_CHANGE_DELAY) { 526 // return true; 527 //} 528 // Check the mask lifetime 529 if ($this->maskLifetime() >= self::IP_CHANGE_DELAY) { 530 return true; 531 } 532 // Otherwise do a more strict activity check 533 if ($this->postCount() >= 9 || $this->reportCount() >= 20) { 534 return true; 535 } 536 } 537 538 // All checks failed 539 return false; 540 } 541 542 public function isUserKnownOrVerified($for_minutes = 1440, $since_ts = 0) { 543 if ($this->verifiedLevel()) { 544 return true; 545 } 546 547 return $this->isUserKnown($for_minutes, $since_ts); 548 } 549 550 public function updatePostActivity($is_thread, $has_file, $is_dummy = false) { 551 $actions = self::A_POST; 552 553 if ($is_thread) { 554 $actions = $actions | self::A_THREAD; 555 } 556 557 if ($has_file) { 558 $actions = $actions | self::A_IMG; 559 } 560 561 $this->updateActivity($actions, $is_dummy); 562 } 563 564 public function updateReportActivity($is_dummy = false) { 565 $this->updateActivity(self::A_REPORT, $is_dummy); 566 } 567 568 public function updateActivity($kind, $is_dummy = false) { 569 $this->action_buffer = $this->action_buffer | $kind; 570 571 $ip_change_delta = -1; 572 573 if ($this->idleLifetime() < self::IP_CHANGE_DELAY) { 574 if ($this->maskChanged()) { 575 $ip_change_delta = self::IP_CHANGE_MASK_VAL; 576 } 577 else if ($this->ipChanged()) { 578 $ip_change_delta = self::IP_CHANGE_IP_VAL; 579 } 580 } 581 582 $this->ip_change_score = min(max(0, $this->ip_change_score + $ip_change_delta), self::IP_CHANGE_SCORE_MAX); 583 584 if ($this->ip_change_score >= self::IP_CHANGE_SCORE_MAX) { 585 $this->resetActionCounts(); 586 } 587 588 if ($this->action_ts === 0) { 589 $this->action_ts = $this->now; 590 } 591 else if (!$is_dummy && $this->lastActionLifetime() >= self::ACTION_DELAY) { 592 if ($this->action_buffer & self::A_REPORT) { 593 $this->report_count = min($this->report_count + 1, 0xFF); 594 } 595 596 if ($this->action_buffer & self::A_POST) { 597 $this->post_count = min($this->post_count + 1, 0xFF); 598 } 599 600 if ($this->action_buffer & self::A_IMG) { 601 $this->img_count = min($this->img_count + 1, 0xFF); 602 } 603 604 if ($this->action_buffer & self::A_THREAD) { 605 $this->thread_count = min($this->thread_count + 1, 0xFF); 606 } 607 608 $this->action_buffer = 0; 609 610 $this->action_ts = $this->now; 611 } 612 613 $this->activity_ts = $this->now; 614 615 $this->pwd_sig = null; 616 } 617 618 public function postCount() { 619 return $this->post_count + ($this->action_buffer & self::A_POST ? 1 : 0); 620 } 621 622 public function imgCount() { 623 return $this->img_count + ($this->action_buffer & self::A_IMG ? 1 : 0); 624 } 625 626 public function threadCount() { 627 return $this->thread_count + ($this->action_buffer & self::A_THREAD ? 1 : 0); 628 } 629 630 public function reportCount() { 631 return $this->report_count + ($this->action_buffer & self::A_REPORT ? 1 : 0); 632 } 633 634 public function ipChangeScore() { 635 return $this->ip_change_score; 636 } 637 638 // Never used 639 public function isNeverUsed() { 640 return $this->activity_ts === 0; 641 } 642 643 // Used only once 644 public function isUsedOnlyOnce() { 645 return $this->activity_ts === $this->creation_ts; 646 } 647 648 // Just created 649 public function isNew() { 650 return $this->creation_ts === $this->now; 651 } 652 653 // Fake or spoofed 654 public function isFake() { 655 return $this->errno === self::E_PWDSIG; 656 } 657 658 public function getEncodedData() { 659 if (!$this->domain || !$this->ip) { 660 return false; 661 } 662 663 $data = []; 664 665 // Raw password 666 if ($this->pwd_raw) { 667 $data[] = $this->pwd_raw; 668 } 669 else { 670 return false; 671 } 672 673 // Creation timestamp 674 if ($this->creation_ts > 0) { 675 $data[] = pack('V', $this->creation_ts); 676 } 677 else { 678 return false; 679 } 680 681 // Mask timestamp 682 if ($this->mask_ts > 0) { 683 $data[] = pack('V', $this->mask_ts); 684 } 685 else { 686 return false; 687 } 688 689 // IP timestamp 690 if ($this->ip_ts > 0) { 691 $data[] = pack('V', $this->ip_ts); 692 } 693 else { 694 return false; 695 } 696 697 // Last ativity timestamp 698 if ($this->activity_ts < 0) { 699 return false; 700 } 701 702 $data[] = pack('V', $this->activity_ts); 703 704 // Last action increment timestamp 705 if ($this->action_ts < 0) { 706 return false; 707 } 708 709 $data[] = pack('V', $this->action_ts); 710 711 // Env timestamp 712 if ($this->env_ts > 0) { 713 $data[] = pack('V', $this->env_ts); 714 } 715 else { 716 return false; 717 } 718 719 // Verified level 720 if ($this->verified_level < 0) { 721 return false; 722 } 723 724 $data[] = pack('C', $this->verified_level); 725 726 // Action counts 727 $data[] = pack('C5', $this->post_count, $this->img_count, $this->thread_count, $this->report_count, $this->action_buffer); 728 729 // IP change score 730 $data[] = pack('C', $this->ip_change_score); 731 732 // Password signature 733 if ($this->pwd_sig) { 734 $data[] = $this->pwd_sig; 735 } 736 else { 737 $data[] = $this->calc_sig([ 738 $this->pwd_raw, $this->creation_ts, $this->activity_ts, $this->action_ts, $this->env_ts, $this->verified_level, 739 $this->post_count, $this->img_count, $this->thread_count, $this->report_count, $this->action_buffer, $this->ip_change_score, 740 $this->domain 741 ]); 742 } 743 744 // Mask signature 745 if ($this->mask_sig) { 746 $data[] = $this->mask_sig; 747 } 748 else { 749 $data[] = $this->calc_sig([ $this->pwd_raw, $this->mask_ts, $this->get_ip_mask($this->ip), $this->domain ]); 750 } 751 752 // IP signature 753 if ($this->ip_sig) { 754 $data[] = $this->ip_sig; 755 } 756 else { 757 $data[] = $this->calc_sig([ $this->pwd_raw, $this->ip_ts, $this->ip, $this->domain ]); 758 } 759 760 // Env signature 761 if ($this->env_sig) { 762 $data[] = $this->env_sig; 763 } 764 else { 765 $data[] = $this->calc_sig([ $this->pwd_raw, $this->env_ts, $this->env_data, $this->domain ]); 766 } 767 768 // --- 769 770 $data = implode('', $data); 771 772 list($data, $nonce) = self::encrypt($data); 773 774 if (!$data) { 775 return false; 776 } 777 778 // Version + Nonce 779 $data = pack('C', self::VERSION) . $nonce . $data; 780 781 return self::b64_encode($data); 782 } 783 784 private static function encrypt($data) { 785 $data_len = strlen($data); 786 787 $key = hex2bin(self::XOR_KEY); 788 $nonce = openssl_random_pseudo_bytes(self::NONCE_SIZE); 789 790 if (!$data_len || !$nonce || $data_len > strlen($key)) { 791 return false; 792 } 793 794 $output_nonced = ''; 795 796 // Apply nonce 797 $ni = 0; 798 799 for ($di = 0; $di < $data_len; ++$di) { 800 if ($ni >= self::NONCE_SIZE) { 801 $ni = 0; 802 } 803 804 $output_nonced = $output_nonced . ($data[$di] ^ $nonce[$ni]); 805 806 $ni++; 807 } 808 809 $output = ''; 810 811 // XOR Encrypt 812 for ($i = 0; $i < $data_len; ++$i) { 813 $output = $output . ($output_nonced[$i] ^ $key[$i]); 814 } 815 816 return [ $output, $nonce ]; 817 } 818 819 private static function decrypt($data, $nonce) { 820 $data_len = strlen($data); 821 822 $nonce_len = strlen($nonce); 823 824 $key = hex2bin(self::XOR_KEY); 825 826 if (!$data_len || !$nonce || $data_len > strlen($key)) { 827 return false; 828 } 829 830 $output_nonced = ''; 831 832 // XOR Decrypt 833 for ($i = 0; $i < $data_len; ++$i) { 834 $output_nonced = $output_nonced . ($data[$i] ^ $key[$i]); 835 } 836 837 // Apply nonce 838 $output = ''; 839 840 $ni = 0; 841 842 for ($di = 0; $di < $data_len; ++$di) { 843 if ($ni >= $nonce_len) { 844 $ni = 0; 845 } 846 847 $output = $output . ($output_nonced[$di] ^ $nonce[$ni]); 848 849 $ni++; 850 } 851 852 return $output; 853 } 854 855 private function generate() { 856 if (!$this->ip || !$this->domain) { 857 return false; 858 } 859 860 $pwd_raw = openssl_random_pseudo_bytes(self::PWD_SIZE); 861 862 if (!$pwd_raw) { 863 return false; 864 } 865 866 $this->version = self::VERSION; 867 868 $this->pwd_raw = $pwd_raw; 869 $this->pwd_hex = bin2hex($pwd_raw); 870 $this->creation_ts = $this->now; 871 $this->mask_ts = $this->now; 872 $this->ip_ts = $this->now; 873 $this->env_ts = $this->now; 874 875 return true; 876 } 877 878 public function setPwd($pwd_hex) { 879 if (!$pwd_hex) { 880 return false; 881 } 882 883 $pwd_raw = hex2bin($pwd_hex); 884 885 if (!$pwd_raw || strlen($pwd_raw) !== self::PWD_SIZE) { 886 return false; 887 } 888 889 $this->pwd_raw = $pwd_raw; 890 $this->pwd_hex = $pwd_hex; 891 892 $this->resetSignatures(); 893 894 return true; 895 } 896 897 public function setVerifiedLevel($level) { 898 if ($level < 0) { 899 return false; 900 } 901 $this->verified_level = $level; 902 $this->pwd_sig = null; 903 } 904 905 private function resetTimestamps() { 906 $this->creation_ts = $this->now; 907 $this->mask_ts = $this->now; 908 $this->ip_ts = $this->now; 909 $this->action_ts = $this->now; 910 $this->activity_ts = 0; 911 $this->env_ts = $this->now; 912 } 913 914 private function resetActionCounts() { 915 $this->post_count = 0; 916 $this->img_count = 0; 917 $this->thread_count = 0; 918 $this->report_count = 0; 919 920 $this->action_buffer = 0; 921 } 922 923 private function resetSignatures() { 924 $this->pwd_sig = null; 925 $this->mask_sig = null; 926 $this->ip_sig = null; 927 $this->env_sig = null; 928 } 929 930 public function setCookie($domain) { 931 $data = $this->getEncodedData(); 932 933 if ($data) { 934 return setcookie(self::COOKIE_NAME, $data, $this->now + self::COOKIE_TTL, '/', $domain, true, true); 935 } 936 else { 937 return false; 938 } 939 } 940 941 public static function setFakeCookie($now, $domain) { 942 $size = self::NONCE_SIZE + self::PWD_SIZE + self::DATA_SIZE + self::SIG_SIZE * self::SIG_COUNT; 943 944 $data = openssl_random_pseudo_bytes($size); 945 946 if (!$data) { 947 return false; 948 } 949 950 $data = pack('C', self::VERSION) . $data; 951 952 $data = self::b64_encode($data); 953 954 return setcookie(self::COOKIE_NAME, $data, $now + self::COOKIE_TTL, '/', $domain, true); 955 } 956 }