Replace captcha package with own implementation

This commit is contained in:
2026-04-20 21:36:02 +02:00
parent 361b511c3e
commit 8ae9eaaadb
17 changed files with 212 additions and 156 deletions

View File

@@ -5,7 +5,7 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Downloads;
use App\Models\Episode;
use GrantHolle\Altcha\Rules\ValidAltcha;
use App\Rules\ValidCaptcha;
use Illuminate\Http\Request;
class DownloadApiController extends Controller
@@ -17,7 +17,7 @@ class DownloadApiController extends Controller
{
$validated = $request->validate([
'episode_id' => ['required'],
'captcha' => ['required', new ValidAltcha],
'captcha' => ['required', new ValidCaptcha],
]);
$episode = Episode::where('id', $request->input('episode_id'))

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Auth;
use AltchaOrg\Altcha\Algorithm\Pbkdf2;
use AltchaOrg\Altcha\Altcha;
use AltchaOrg\Altcha\CreateChallengeOptions;
use App\Http\Controllers\Controller;
class CaptchaController extends Controller
{
public function create(): array
{
$pbkdf2 = new Pbkdf2;
$altcha = new Altcha(
hmacSignatureSecret: config('captcha.hmac_key'),
);
// Create challenge
$challenge = $altcha->createChallenge(new CreateChallengeOptions(
algorithm: $pbkdf2,
cost: 5000,
counter: random_int(5000, 10000),
expiresAt: time() + 600,
));
return get_object_vars($challenge);
}
}

View File

@@ -4,7 +4,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use GrantHolle\Altcha\Rules\ValidAltcha;
use App\Rules\ValidCaptcha;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -25,7 +25,7 @@ class RegisteredUserController extends Controller
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
'altcha' => ['required', new ValidAltcha],
'altcha' => ['required', new ValidCaptcha],
]);
$user = User::create([

View File

@@ -3,7 +3,7 @@
namespace App\Http\Controllers;
use App\Models\Contact;
use GrantHolle\Altcha\Rules\ValidAltcha;
use App\Rules\ValidCaptcha;
use Illuminate\Http\Request;
class ContactController extends Controller
@@ -26,7 +26,7 @@ class ContactController extends Controller
'email' => 'required|max:50',
'message' => 'required|max:1000',
'subject' => 'required|max:50',
'altcha' => ['required', new ValidAltcha],
'altcha' => ['required', new ValidCaptcha],
]);
$contact = new Contact;

View File

@@ -2,7 +2,7 @@
namespace App\Http\Requests\Auth;
use GrantHolle\Altcha\Rules\ValidAltcha;
use App\Rules\ValidCaptcha;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
@@ -30,7 +30,7 @@ class LoginRequest extends FormRequest
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
'altcha' => ['required', new ValidAltcha],
'altcha' => ['required', new ValidCaptcha],
];
}

129
app/Rules/ValidCaptcha.php Normal file
View File

@@ -0,0 +1,129 @@
<?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;
/**
* 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=): \Illuminate\Translation\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.');
}
}
}

View File

@@ -9,7 +9,7 @@
"license": "MIT",
"require": {
"php": "^8.2",
"grantholle/laravel-altcha": "^2.1",
"altcha-org/altcha": "^2.0",
"guzzlehttp/guzzle": "^7.8.1",
"hisorange/browser-detect": "^5.0",
"http-interop/http-factory-guzzle": "^1.2",

96
composer.lock generated
View File

@@ -4,24 +4,23 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3400111a6254560d548639295422875c",
"content-hash": "a33ae97980a40b1a7c55bec75b10d64f",
"packages": [
{
"name": "altcha-org/altcha",
"version": "v1.3.1",
"version": "v2.0.0",
"source": {
"type": "git",
"url": "https://github.com/altcha-org/altcha-lib-php.git",
"reference": "9e9e70c864a9db960d071c77c778be0c9ff1a4d0"
"reference": "8fc4a698b159824d7c1d729dc08e5c0687b494a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/altcha-org/altcha-lib-php/zipball/9e9e70c864a9db960d071c77c778be0c9ff1a4d0",
"reference": "9e9e70c864a9db960d071c77c778be0c9ff1a4d0",
"url": "https://api.github.com/repos/altcha-org/altcha-lib-php/zipball/8fc4a698b159824d7c1d729dc08e5c0687b494a9",
"reference": "8fc4a698b159824d7c1d729dc08e5c0687b494a9",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=8.2"
},
"require-dev": {
@@ -29,7 +28,7 @@
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^11.5"
"phpunit/phpunit": "^11.5.50"
},
"type": "library",
"autoload": {
@@ -47,11 +46,12 @@
"email": "536331+ovx@users.noreply.github.com"
}
],
"description": "A lightweight PHP library for creating and verifying ALTCHA challenges.",
"support": {
"issues": "https://github.com/altcha-org/altcha-lib-php/issues",
"source": "https://github.com/altcha-org/altcha-lib-php/tree/v1.3.1"
"source": "https://github.com/altcha-org/altcha-lib-php/tree/v2.0.0"
},
"time": "2025-12-13T10:03:53+00:00"
"time": "2026-04-07T11:35:25+00:00"
},
{
"name": "brick/math",
@@ -823,82 +823,6 @@
],
"time": "2025-12-27T19:43:20+00:00"
},
{
"name": "grantholle/laravel-altcha",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/grantholle/laravel-altcha.git",
"reference": "c0dcc6d0805e8640d46709e5f8d05c7c65b2687c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/grantholle/laravel-altcha/zipball/c0dcc6d0805e8640d46709e5f8d05c7c65b2687c",
"reference": "c0dcc6d0805e8640d46709e5f8d05c7c65b2687c",
"shasum": ""
},
"require": {
"altcha-org/altcha": "^1.3.1",
"illuminate/contracts": "^10.0|^11.0|^12.0",
"php": "^8.2",
"spatie/laravel-package-tools": "^1.14.0"
},
"require-dev": {
"laravel/pint": "^1.0",
"nunomaduro/collision": "^8.1.1||^7.10.0",
"orchestra/testbench": "^10.0.0||^9.0.0||^8.22.0",
"pestphp/pest": "^3.0||^2.0",
"pestphp/pest-plugin-arch": "^3.0||^2.0",
"pestphp/pest-plugin-laravel": "^3.0||^2.0",
"spatie/laravel-ray": "^1.26"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Altcha": "GrantHolle\\Altcha\\Facades\\Altcha"
},
"providers": [
"GrantHolle\\Altcha\\AltchaServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"GrantHolle\\Altcha\\": "src/",
"GrantHolle\\Altcha\\Database\\Factories\\": "database/factories/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Grant Holle",
"email": "hollegrant@gmail.com",
"role": "Developer"
}
],
"description": "A Laravel server implementation for Altcha.",
"homepage": "https://github.com/grantholle/laravel-altcha",
"keywords": [
"Grant Holle",
"laravel",
"laravel-altcha"
],
"support": {
"issues": "https://github.com/grantholle/laravel-altcha/issues",
"source": "https://github.com/grantholle/laravel-altcha/tree/2.1.1"
},
"funding": [
{
"url": "https://github.com/Grant Holle",
"type": "github"
}
],
"time": "2025-12-16T03:39:06+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "7.10.0",
@@ -11584,5 +11508,5 @@
"php": "^8.2"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}

9
config/captcha.php Normal file
View File

@@ -0,0 +1,9 @@
<?php
/**
* Altcha Captcha System
*/
return [
'hmac_key' => env('ALTCHA_HMAC_KEY'),
];

39
package-lock.json generated
View File

@@ -4,12 +4,11 @@
"requires": true,
"packages": {
"": {
"name": "hstream",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.1",
"@jellyfin/libass-wasm": "^4.1.1",
"@yaireo/tagify": "^4.21.2",
"altcha": "^2.3.0",
"altcha": "^3.0.0",
"chart.js": "^4.5.0",
"dashjs": "^5.0.0",
"hammerjs": "^2.0.8",
@@ -42,12 +41,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@altcha/crypto": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@altcha/crypto/-/crypto-0.0.1.tgz",
"integrity": "sha512-qZMdnoD3lAyvfSUMNtC2adRi666Pxdcw9zqfMU5qBOaJWqpN9K+eqQGWqeiKDMqL0SF+EytNG4kR/Pr/99GJ6g==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
@@ -1209,30 +1202,14 @@
}
},
"node_modules/altcha": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/altcha/-/altcha-2.3.0.tgz",
"integrity": "sha512-vl8I0dQvSQB7/Mx09XuWZ1+LdSP7vEda6OLbg9kUQ2ZO2LT7MzgUyLK7Iips+GAV6c0ntVcS1XWOqhEPpwbDhQ==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/altcha/-/altcha-3.0.4.tgz",
"integrity": "sha512-TA1N0vnKR7aI/usDN2y3dPDcg9DdsDQKvRuCwApMtw3kJHjxH7NRtt+hSV8NZ01tkfHqXEJOtCYRFr8sxw8Fbg==",
"license": "MIT",
"dependencies": {
"@altcha/crypto": "^0.0.1"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.18.0"
"hash-wasm": "^4.12.0"
}
},
"node_modules/altcha/node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz",
"integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@@ -2004,6 +1981,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash-wasm": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz",
"integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==",
"license": "MIT"
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",

View File

@@ -20,7 +20,7 @@
"@fortawesome/fontawesome-free": "^6.5.1",
"@jellyfin/libass-wasm": "^4.1.1",
"@yaireo/tagify": "^4.21.2",
"altcha": "^2.3.0",
"altcha": "^3.0.0",
"chart.js": "^4.5.0",
"dashjs": "^5.0.0",
"hammerjs": "^2.0.8",

View File

@@ -126,29 +126,5 @@ input:checked~.dot {
/* Captcha */
:root {
--altcha-border-width: 1px;
--altcha-border-radius: 0.375rem;
--altcha-color-base: #333;
--altcha-color-border: #a0a0a0;
--altcha-color-text: #fff;
--altcha-color-border-focus: currentColor;
--altcha-color-error-text: #f23939;
--altcha-color-footer-bg: #141414;
--altcha-max-width: 260px;
color-scheme: light dark;
}
.altcha-footer {
border-bottom-left-radius: 0.375rem;
border-bottom-right-radius: 0.375rem;
}
input[type="checkbox"] {
background-color: #ffffff;
border-color: #a0a0a0;
color: rgb(225,29,72);
}
input[type="checkbox"]:checked {
background-color: rgb(225,29,72);
box-shadow: 0 0 0 0px #fff, 0 0 0 calc(2px + 0px) rgba(246, 59, 118, 0.5), 0 0 #0000;
}

View File

@@ -14,6 +14,7 @@ import {
// Captcha
import 'altcha';
import "altcha/themes/cupcake.css";
// import Alpine from 'alpinejs';

View File

@@ -69,8 +69,8 @@
</label>
</div>
<div class="block">
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
<div class="block pt-3 w-3/4 mx-auto">
<altcha-widget id="captcha" theme="cupcake" challenge="/altcha-challenge"></altcha-widget>
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
</div>
@@ -132,8 +132,8 @@
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="block">
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
<div class="block pt-3 w-3/4 mx-auto">
<altcha-widget id="captcha" theme="cupcake" challenge="/altcha-challenge"></altcha-widget>
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
</div>

View File

@@ -33,7 +33,7 @@
<x-input-error class="mt-2" :messages="$errors->get('message')" />
</div>
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
<altcha-widget id="captcha" theme="cupcake" challenge="/altcha-challenge"></altcha-widget>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Submit') }}</x-primary-button>

View File

@@ -7,8 +7,8 @@
</p>
<p id="message" class="text-red-600">
</p>
<div class="flex pt-2">
<altcha-widget id="altcha" challengeurl="/altcha-challenge"></altcha-widget>
<div class="block pt-2">
<altcha-widget id="altcha" theme="cupcake" challenge="/altcha-challenge"></altcha-widget>
</div>
<br>
<p class="text-gray-800 dark:text-gray-200 text-sm">

View File

@@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\CaptchaController;
use App\Http\Controllers\Auth\ConfirmablePasswordController;
use App\Http\Controllers\Auth\DiscordAuthController;
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
@@ -38,6 +39,9 @@ Route::middleware('guest')->group(function () {
->name('discord.login');
Route::get('/auth/discord/callback', [DiscordAuthController::class, 'callback']);
// Captcha
Route::get('/altcha-challenge', [CaptchaController::class, 'create']);
});
Route::middleware('auth')->group(function () {