131 lines
3.1 KiB
PHP
131 lines
3.1 KiB
PHP
<?php
|
|
|
|
namespace App\Rules;
|
|
|
|
use AltchaOrg\Altcha\Algorithm\Pbkdf2;
|
|
use AltchaOrg\Altcha\Altcha;
|
|
use AltchaOrg\Altcha\Challenge;
|
|
use AltchaOrg\Altcha\ChallengeParameters;
|
|
use AltchaOrg\Altcha\Payload;
|
|
use AltchaOrg\Altcha\Solution;
|
|
use AltchaOrg\Altcha\VerifySolutionOptions;
|
|
use Closure;
|
|
use Illuminate\Contracts\Validation\ValidationRule;
|
|
use Illuminate\Translation\PotentiallyTranslatedString;
|
|
|
|
/**
|
|
* Validation rule to verify captcha solution.
|
|
*/
|
|
class ValidCaptcha implements ValidationRule
|
|
{
|
|
/**
|
|
* Altcha instance.
|
|
*/
|
|
protected Altcha $altcha;
|
|
|
|
/**
|
|
* Pbkdf2 algorithm instance.
|
|
*/
|
|
protected Pbkdf2 $pbkdf2;
|
|
|
|
/**
|
|
* Class constructor.
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->pbkdf2 = new Pbkdf2;
|
|
$this->altcha = new Altcha(
|
|
hmacSignatureSecret: config('captcha.hmac_key'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Parse payload and return the decoded data as an array.
|
|
*/
|
|
private function parsePayload(string $value): ?array
|
|
{
|
|
$decoded = base64_decode($value, true);
|
|
if ($decoded === false) {
|
|
return null;
|
|
}
|
|
|
|
$payload = json_decode($decoded, true);
|
|
if (! is_array($payload)) {
|
|
return null;
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* Verify if payload has required fields.
|
|
*/
|
|
private function verifyFields(array $payload): bool
|
|
{
|
|
if (! isset($payload['challenge'], $payload['solution'])) {
|
|
return false;
|
|
}
|
|
|
|
if (! is_array($payload['challenge']) || ! is_array($payload['solution'])) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Create a Challenge object from challenge data.
|
|
*/
|
|
private function createChallenge(array $challengeData): Challenge
|
|
{
|
|
return new Challenge(
|
|
ChallengeParameters::fromArray($challengeData['parameters'] ?? []),
|
|
$challengeData['signature'] ?? null,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create a Solution object from solution data.
|
|
*/
|
|
private function createSolution(array $solutionData): Solution
|
|
{
|
|
return new Solution(
|
|
counter: (int) ($solutionData['counter'] ?? 0),
|
|
derivedKey: (string) ($solutionData['derivedKey'] ?? ''),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Run the validation rule.
|
|
*
|
|
* @param Closure(string, ?string=): PotentiallyTranslatedString $fail
|
|
*/
|
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
|
{
|
|
$payload = $this->parsePayload($value);
|
|
if (! $payload) {
|
|
$fail('Invalid captcha.');
|
|
|
|
return;
|
|
}
|
|
|
|
if (! $this->verifyFields($payload)) {
|
|
$fail('Invalid captcha.');
|
|
|
|
return;
|
|
}
|
|
|
|
$challenge = $this->createChallenge($payload['challenge']);
|
|
$solution = $this->createSolution($payload['solution']);
|
|
|
|
$result = $this->altcha->verifySolution(new VerifySolutionOptions(
|
|
algorithm: $this->pbkdf2,
|
|
payload: new Payload($challenge, $solution),
|
|
));
|
|
|
|
if (! $result->verified) {
|
|
$fail('Invalid captcha.');
|
|
}
|
|
}
|
|
}
|