secret = (string) config('physical_attendance_gate.secret'); $this->period = max(30, (int) config('physical_attendance_gate.period_seconds', 60)); } public function isConfigured() { return $this->secret !== ''; } /** * Current code and countdown for the counter display. * * @param int $electionId * @return array{code:string,seconds_remaining:int,period_seconds:int} */ public function currentPayload($electionId) { $slot = $this->currentSlot(); $code = $this->codeForSlot((int) $electionId, $slot); $expiresAt = ($slot + 1) * $this->period; $secondsRemaining = max(0, $expiresAt - time()); return [ 'code' => $code, 'seconds_remaining' => $secondsRemaining, 'period_seconds' => $this->period, ]; } /** * Accept current, previous, or next slot to tolerate minor clock skew. * * @param int $electionId * @param string $input * @return bool */ public function validates($electionId, $input) { if (!$this->isConfigured()) { return false; } $normalized = strtoupper(preg_replace('/\s+/', '', (string) $input)); if (strlen($normalized) < 4) { return false; } $slot = $this->currentSlot(); foreach ([$slot - 1, $slot, $slot + 1] as $s) { if ($s < 0) { continue; } if (hash_equals($this->codeForSlot((int) $electionId, $s), $normalized)) { return true; } } return false; } protected function currentSlot() { return intdiv(time(), $this->period); } /** * Deterministic code for one election and time slot. * * @param int $electionId * @param int $slot * @return string */ protected function codeForSlot($electionId, $slot) { $material = (string) $electionId.'|'.(string) $slot; $hex = hash_hmac('sha256', $material, $this->secret); return strtoupper(substr($hex, 0, 8)); } }