"localhost", 'username' => "test", 'password' => "test", 'database' => "Test", ]; $this->database = new PDO( "mysql:dbname=".$config['database'].";host=".$config['host'].";charset=utf8", $config['username'], $config['password']); } public function getId(): ?int { return $this->id; } public function getCreateDatetime(): ?\DateTimeInterface { return $this->create_datetime; } public function setCreateDatetime(): static { $this->create_datetime = new \DateTime(); return $this; } public function setSaltParam(array $params): static { $this->salt_params = $params; return $this; } public function clearSaltParam(): static { $this->salt_params = []; return $this; } public function addSaltParam(string $key, mixed $value) { $this->salt_params[$key] = $value; } public function removeSaltParam(string $key) { unset($this->salt_params[$key]); } public function getSalt(): ?string { return $this->salt; } public function setSalt(): static { $chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; $chars_length = strlen($chars); $salt = ""; $length = 32; for ($i = 0; $i < $length; $i++) { $salt .= $chars[random_int(0, $chars_length - 1)]; } $this->salt = $salt; return $this; } public function generateSalt(): static { $this->salt_with_params = $this->salt.(($this->set_expire_timestamp)?"?expires=".$this->expire_timestamp:""); if (!empty($this->salt_params)) { $first = true; foreach ($this->salt_params AS $key => $value) { $this->salt_with_params .= (((!$this->set_expire_timestamp) && ($first))?"?":"&").$key."=".$value; $first = false; } } return $this; } public function getSecretNumber(): ?int { return $this->secret_number; } public function setSecretNumber(): static { $this->secret_number = random_int($this->min_number, $this->max_number - 1); return $this; } public function getAlgorithm(): ?string { return $this->algorithm; } public function setAlgorithm(string $algorithm): static { $this->algorithm = $algorithm; return $this; } public function calcChallenge(): static { $this->challenge = hash($this->algorithm, $this->salt_with_params.$this->secret_number); while (!$this->checkChallengeNotExists()) { $this->setSalt(); $this->setSecretNumber(); $this->challenge = hash($this->algorithm, $this->salt_with_params.$this->secret_number); } return $this; } public function getChallenge(): ?string { return $this->challenge; } public function getSignature(): ?string { return $this->signature; } public function setSignature(): static { $this->signature = hash_hmac($this->algorithm, $this->challenge, $this->hmac_key); return $this; } public function getSetExpireTimestamp(): ? bool { return $this->set_expire_timestamp; } public function setSetExpireTimestamp(bool $set_expire_timestamp) { $this->set_expire_timestamp = $set_expire_timestamp; return $this; } public function getExpireTimestamp(): ?int { return $this->expire_timestamp; } public function setExpireTimestamp(bool $set_expire_timestamp = true): static { $this->set_expire_timestamp = $set_expire_timestamp; if ($this->set_expire_timestamp) { $create_timestamp = date_timestamp_get($this->create_datetime); $this->expire_timestamp = $create_timestamp + $this->expire_time; } return $this; } public function getChecked(): bool { return $this->checked; } public function setChecked(bool $checked): static { $this->checked = $checked; return $this; } public function getPublicArray(): array { $algorithm = [ 'sha128' => "SHA-128", 'sha256' => "SHA-256", 'sha512' => "SHA-512" ]; $return = [ 'id' => $this->id, '_create_datetime' => $this->create_datetime, 'salt' => $this->salt_with_params, 'challenge' => $this->challenge, 'algorithm' => ((array_key_exists($this->algorithm, $algorithm))?$algorithm[$this->algorithm]:$this->algorithm), 'signature' => $this->signature, ]; if ($this->return_max_number) { $return['maxnumber'] = $this->max_number; } return $return; } public function createDBTable() { $query = "CREATE TABLE `altcha_challenge` ( `id` INT NOT NULL AUTO_INCREMENT, `create_datetime` DATETIME NOT NULL, `salt_params` JSON NULL DEFAULT NULL, `salt` VARCHAR(64) NOT NULL, `secret_number` INT UNSIGNED NOT NULL, `algorithm_` VARCHAR(10) NOT NULL, `challenge` VARCHAR(64) NOT NULL, `signature_` VARCHAR(64) NOT NULL, `checked` BOOLEAN NOT NULL, PRIMARY KEY (`id`) ) ENGINE = InnoDB;"; $this->database->query($query) ->execute(); } public function insertDB() { $return = false; $query = "INSERT INTO `altcha_challenge` SET create_datetime = :create_datetime, salt_params = :salt_params, salt = :salt, secret_number = :secret_number, algorithm_ = :algorithm_, challenge = :challenge, signature_ = :signature_, checked = false"; $params = [ ':create_datetime' => $this->create_datetime->format('Y-m-d H:i:s'), ':salt_params' => json_encode($this->salt_params), ':salt' => $this->salt, ':secret_number' => $this->secret_number, ':algorithm_' => $this->algorithm, ':challenge' => $this->challenge, ':signature_' => $this->signature, ]; $stmt = $this->database->prepare($query); foreach ($params AS $column => $value) { $stmt->bindValue($column, $value); } $stmt->execute(); if ($stmt->rowCount() == 1) { $return = (($this->database->lastInsertId() != 0)?$this->database->lastInsertId():$stmt->rowCount()); } else { throw new Exception( "Error by execute db insert", 500); } return $return; } private function checkChallengeNotExists() { $query = "SELECT id FROM `altcha_challenge` WHERE salt = :salt AND challenge = :challenge AND signature_ = :signature_ AND checked = false"; $params = [ ':salt' => $this->salt, ':challenge' => $this->challenge, ':signature_' => $this->signature, ]; $stmt = $this->database->prepare($query); foreach ($params AS $column => $value) { $stmt->bindValue($column, $value); } $stmt->execute(); return ($stmt->rowCount() == 0); } public function findUncheckedChallenge($payload) { $decodedPayload = json_decode(base64_decode($payload)); $payloadSalt = explode("?", $decodedPayload->salt)[0]; $query = "SELECT id, create_datetime, salt_params, salt, secret_number, algorithm_, challenge, signature_, checked FROM `altcha_challenge` WHERE salt = :salt AND challenge = :challenge AND signature_ = :signature_ AND secret_number = :secret_number AND checked = false"; $params = [ ':salt' => $payloadSalt, ':challenge' => $decodedPayload->challenge, ':signature_' => $decodedPayload->signature, ':secret_number' => $decodedPayload->number, ]; $stmt = $this->database->prepare($query); foreach ($params AS $column => $value) { $stmt->bindValue($column, $value); } $stmt->execute(); if ($stmt->rowCount() == 1) { $return = $stmt->fetch(PDO::FETCH_ASSOC); $this->id = $return['id']; $this->create_datetime = new DateTime($return['create_datetime']); $this->salt_params = ((!$return['salt_params'])?[]:json_decode($return['salt_params'])); $this->salt = $return['salt']; $this->secret_number = $return['secret_number']; $this->algorithm = $return['algorithm_']; $this->challenge = $return['challenge']; $this->signature = $return['signature_']; $this->checked = $return['checked']; return true; } unset($stmt); return false; } public function setChallengeChecked() { if ($this->id) { $query = "UPDATE `altcha_challenge` SET checked = true WHERE id = :id AND checked = false"; $stmt = $this->database->prepare($query); $stmt->bindValue(':id', $this->id); $stmt->execute(); if ($stmt->rowCount() == 1) { return true; } else { throw new Exception("error on set altcha challenge checked"); } } else { throw new Exception("no altcha challenge available"); } return false; } // Call from Cronjob public function deleteOlderChallenges() { $query = "DELETE FROM `altcha_challenge` WHERE create_datetime < :oldest_datetime"; $stmt = $this->database->prepare($query); $date = new \DateTime(); $oldest_datetime = $date->modify("-".$this->days_save_challenge.' days'); $stmt->bindValue(':oldest_datetime', $oldest_datetime->format("Y-m-d H:i:s")); $stmt->execute(); return $stmt->rowCount(); } } if ($_GET['altcha'] == "create_db_table") { $challende = new AltchaChallenge(); var_dump($challende->createDBTable()); } else if ($_GET['altcha'] == "open_challenge") { $challenge = new AltchaChallenge(); $challenge->setCreateDatetime() ->setExpireTimestamp() ->setSalt() ->generateSalt() ->setSecretNumber() ->calcChallenge() ->setSignature() ->insertDB(); echo json_encode($challenge->getPublicArray(), JSON_PRETTY_PRINT); } else if (($_GET['altcha'] == "check_challenge") && (isset($_POST['altcha']))) { $request = $_POST['altcha']; $challenge = new AltchaChallenge(); $return = $challenge->findUncheckedChallenge($request); (($return)?$challenge->setChallengeChecked():null); var_dump($return); } else if ($_GET['altcha'] == "delete_old") { $challende = new AltchaChallenge(); $challende->deleteOlderChallenges(); } ?>