/ lib / userpwd-test.php
userpwd-test.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  }