From 8ae9eaaadbfd128de569ce901e69e497cba7ad37 Mon Sep 17 00:00:00 2001 From: w33b Date: Mon, 20 Apr 2026 21:36:02 +0200 Subject: [PATCH] Replace captcha package with own implementation --- .../Controllers/Api/DownloadApiController.php | 4 +- .../Controllers/Auth/CaptchaController.php | 30 ++++ .../Auth/RegisteredUserController.php | 4 +- app/Http/Controllers/ContactController.php | 4 +- app/Http/Requests/Auth/LoginRequest.php | 4 +- app/Rules/ValidCaptcha.php | 129 ++++++++++++++++++ composer.json | 2 +- composer.lock | 96 ++----------- config/captcha.php | 9 ++ package-lock.json | 39 ++---- package.json | 2 +- resources/css/app.css | 26 +--- resources/js/app.js | 1 + resources/views/auth/login.blade.php | 8 +- .../partials/submit-contact-form.blade.php | 2 +- .../partials/download-captcha.blade.php | 4 +- routes/auth.php | 4 + 17 files changed, 212 insertions(+), 156 deletions(-) create mode 100644 app/Http/Controllers/Auth/CaptchaController.php create mode 100644 app/Rules/ValidCaptcha.php create mode 100644 config/captcha.php diff --git a/app/Http/Controllers/Api/DownloadApiController.php b/app/Http/Controllers/Api/DownloadApiController.php index 2a351d5..e0ded53 100644 --- a/app/Http/Controllers/Api/DownloadApiController.php +++ b/app/Http/Controllers/Api/DownloadApiController.php @@ -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')) diff --git a/app/Http/Controllers/Auth/CaptchaController.php b/app/Http/Controllers/Auth/CaptchaController.php new file mode 100644 index 0000000..13679bc --- /dev/null +++ b/app/Http/Controllers/Auth/CaptchaController.php @@ -0,0 +1,30 @@ +createChallenge(new CreateChallengeOptions( + algorithm: $pbkdf2, + cost: 5000, + counter: random_int(5000, 10000), + expiresAt: time() + 600, + )); + + return get_object_vars($challenge); + } +} diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 5c22d9f..130998e 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -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([ diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php index 74eadcf..44f192d 100644 --- a/app/Http/Controllers/ContactController.php +++ b/app/Http/Controllers/ContactController.php @@ -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; diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php index 769a9e4..f8a71a6 100644 --- a/app/Http/Requests/Auth/LoginRequest.php +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -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], ]; } diff --git a/app/Rules/ValidCaptcha.php b/app/Rules/ValidCaptcha.php new file mode 100644 index 0000000..a755f11 --- /dev/null +++ b/app/Rules/ValidCaptcha.php @@ -0,0 +1,129 @@ +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.'); + } + } +} diff --git a/composer.json b/composer.json index ca13145..e486b87 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 0c39eb6..06be66e 100644 --- a/composer.lock +++ b/composer.lock @@ -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" } diff --git a/config/captcha.php b/config/captcha.php new file mode 100644 index 0000000..5e9fb89 --- /dev/null +++ b/config/captcha.php @@ -0,0 +1,9 @@ + env('ALTCHA_HMAC_KEY'), +]; diff --git a/package-lock.json b/package-lock.json index d514b73..88cc60c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c5fdac1..804dc0d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/resources/css/app.css b/resources/css/app.css index e4367ec..90678d5 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -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; -} \ No newline at end of file diff --git a/resources/js/app.js b/resources/js/app.js index ec2e288..4c9116e 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -14,6 +14,7 @@ import { // Captcha import 'altcha'; +import "altcha/themes/cupcake.css"; // import Alpine from 'alpinejs'; diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 8126840..d48f868 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -69,8 +69,8 @@ -
- +
+
@@ -132,8 +132,8 @@
-
- +
+
diff --git a/resources/views/contact/partials/submit-contact-form.blade.php b/resources/views/contact/partials/submit-contact-form.blade.php index 5e8615e..f8bde1e 100644 --- a/resources/views/contact/partials/submit-contact-form.blade.php +++ b/resources/views/contact/partials/submit-contact-form.blade.php @@ -33,7 +33,7 @@
- +
{{ __('Submit') }} diff --git a/resources/views/modals/partials/download-captcha.blade.php b/resources/views/modals/partials/download-captcha.blade.php index 03196f4..7283eaf 100644 --- a/resources/views/modals/partials/download-captcha.blade.php +++ b/resources/views/modals/partials/download-captcha.blade.php @@ -7,8 +7,8 @@

-
- +
+

diff --git a/routes/auth.php b/routes/auth.php index d91e63c..ad4e9aa 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -1,6 +1,7 @@ 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 () {