feat: self-host TweetNaCl, add server-side PoW captcha (PHP), activate categoriesService

This commit is contained in:
2026-02-06 14:04:24 +01:00
parent 1aa723728e
commit ebb5b2f86d
13 changed files with 210 additions and 20 deletions

View File

@@ -0,0 +1,3 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]

43
docs/pow-server/README.md Normal file
View File

@@ -0,0 +1,43 @@
# PoW Captcha Server
PHP-basierter Proof-of-Work Captcha Server für dgray.io.
## Setup
1. Subdomain `pow.dgray.io` auf den Server zeigen
2. Dateien in das Web-Root kopieren
3. Secret setzen:
```bash
# In .env oder Apache/Nginx config:
SetEnv POW_SECRET $(openssl rand -hex 32)
```
Oder direkt in `config.php` den Wert von `POW_SECRET` ändern.
4. Testen:
```bash
curl https://pow.dgray.io/challenge
```
## Endpoints
### GET /challenge
Gibt eine signierte Challenge zurück.
### POST /verify
Prüft die Lösung. Body (JSON):
```json
{
"challenge": "...",
"difficulty": 4,
"nonce": 12345,
"signature": "...",
"timestamp": 1700000000000
}
```
## Sicherheit
- HMAC-SHA256 signierte Challenges (nicht fälschbar)
- TTL: 2 Minuten
- CORS: nur `https://dgray.io`
- `hash_equals()` gegen Timing-Attacks

View File

@@ -0,0 +1,20 @@
<?php
require __DIR__ . '/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
$challenge = bin2hex(random_bytes(16));
$timestamp = (int)(microtime(true) * 1000);
$signature = hash_hmac('sha256', "$challenge:$timestamp:" . POW_DIFFICULTY, POW_SECRET);
echo json_encode([
'challenge' => $challenge,
'difficulty' => POW_DIFFICULTY,
'timestamp' => $timestamp,
'signature' => $signature,
'expires_in' => POW_TTL_SECONDS,
]);

View File

@@ -0,0 +1,4 @@
<?php
define('POW_SECRET', getenv('POW_SECRET') ?: 'CHANGE_ME_TO_A_RANDOM_64_CHAR_HEX_STRING');
define('POW_DIFFICULTY', 4);
define('POW_TTL_SECONDS', 120);

25
docs/pow-server/index.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://dgray.io');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = rtrim($uri, '/');
switch ($uri) {
case '/challenge':
require __DIR__ . '/challenge.php';
break;
case '/verify':
require __DIR__ . '/verify.php';
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Not found']);
}

View File

@@ -0,0 +1,47 @@
<?php
require __DIR__ . '/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$challenge = $input['challenge'] ?? null;
$difficulty = $input['difficulty'] ?? POW_DIFFICULTY;
$nonce = $input['nonce'] ?? null;
$signature = $input['signature'] ?? null;
$timestamp = $input['timestamp'] ?? null;
if (!$challenge || $nonce === null || !$signature || !$timestamp) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Missing fields']);
exit;
}
$now = (int)(microtime(true) * 1000);
if ($now - $timestamp > POW_TTL_SECONDS * 1000) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Challenge expired']);
exit;
}
$expected = hash_hmac('sha256', "$challenge:$timestamp:$difficulty", POW_SECRET);
if (!hash_equals($expected, $signature)) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Invalid signature']);
exit;
}
$hash = hash('sha256', $challenge . $nonce);
$prefix = str_repeat('0', $difficulty);
if (strpos($hash, $prefix) !== 0) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Invalid proof']);
exit;
}
echo json_encode(['ok' => true]);