diff --git a/composer.json b/composer.json index 13d7c3b..e6a6d60 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { - "require": { - "paragonie/sodium_compat": "^2.1" - } -} + "require": { + "paragonie/sodium_compat": "^2.1", + "ext-curl": "*" + } +} \ No newline at end of file diff --git a/index.php b/index.php index 208df4e..6753693 100644 --- a/index.php +++ b/index.php @@ -1,237 +1,286 @@ -exec($query); - - // Check if the 'public_key' column already exists; if not, add it. - $columns = $db->query("PRAGMA table_info(storage)")->fetchAll(PDO::FETCH_ASSOC); - $hasPublicKeyColumn = false; - - foreach ($columns as $column) { - if ($column['name'] === 'public_key') { - $hasPublicKeyColumn = true; - break; - } - } - - if (!$hasPublicKeyColumn) { - $db->exec("ALTER TABLE storage ADD COLUMN public_key BLOB NOT NULL"); - } -} - -// Initialize SQLite and auto-create the database and table if they don't exist -$db = new PDO('sqlite:/home/retrodig/1024clubdb/storage.db'); -initializeDatabase($db); - -// Helper function to respond with JSON -function respond($status, $message, $data = []) { - http_response_code($status); - echo json_encode(['message' => $message, 'data' => $data]); - exit; -} - -// Enhance Base64 decoding to handle both raw and URL-safe inputs -function safeBase64Decode($input) { - // First, attempt a direct Base64 decode - $decoded = base64_decode($input, true); - if ($decoded !== false && strlen($decoded) === 32) { - return $decoded; - } - - // If the direct decode fails, attempt a URL-safe decode - $urlSafeInput = str_replace(['-', '_'], ['+', '/'], $input); - return base64_decode($urlSafeInput, true); -} - -// Handle Base64 encoding safely -function safeBase64Encode($input) { - return str_replace(['+', '/'], ['-', '_'], base64_encode($input)); -} - -// Check if 'action' is set in the query parameters before proceeding -if (isset($_GET['action'])) { - // Create or get storage space based on pubkey hash - if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_GET['action'] === 'create') { - $pubkey = $_POST['pubkey'] ?? null; - - if (!$pubkey) { - respond(400, "Missing public key"); - } - - // Decode the public key, try both raw and URL-safe Base64 decodings - $decodedPubkey = safeBase64Decode($pubkey); - if ($decodedPubkey === false || strlen($decodedPubkey) !== 32) { - respond(400, "Invalid public key"); - } - - $pubkey_hash = bin2hex(sodium_crypto_generichash($decodedPubkey)); - - $stmt = $db->prepare("SELECT * FROM storage WHERE pubkey_hash = :pubkey_hash"); - $stmt->execute([':pubkey_hash' => $pubkey_hash]); - $existing = $stmt->fetch(PDO::FETCH_ASSOC); - - if ($existing) { - respond(200, "Space already exists", ['pubkey_hash' => $pubkey_hash]); - } - - $stmt = $db->prepare("INSERT INTO storage (pubkey_hash, data, mime_type, public_key) VALUES (:pubkey_hash, '', 'text/plain', :public_key)"); - $stmt->execute([':pubkey_hash' => $pubkey_hash, ':public_key' => $decodedPubkey]); - - respond(201, "Space created", ['pubkey_hash' => $pubkey_hash]); - } - -// Retrieve stored data using the public key or public key hash -if ($_SERVER['REQUEST_METHOD'] === 'GET' && $_GET['action'] === 'retrieve') { - $pubkey = $_GET['pubkey'] ?? null; - $pubkey_hash = $_GET['pubkey_hash'] ?? null; - - if ($pubkey) { - // Replace spaces with '+' to account for unencoded URLs - $pubkey = str_replace(' ', '+', $pubkey); - - // Attempt to decode public key (try both standard and URL-safe Base64) - $decodedPubkey = safeBase64Decode($pubkey); - - if ($decodedPubkey === false || strlen($decodedPubkey) !== 32) { - respond(400, "Failed to decode public key: " . htmlspecialchars($pubkey)); - } - - // Hash the decoded public key - $pubkey_hash = bin2hex(sodium_crypto_generichash($decodedPubkey)); - - } elseif ($pubkey_hash) { - // Validate the provided public key hash (must be 64 characters long, hex format) - if (!ctype_xdigit($pubkey_hash) || strlen($pubkey_hash) !== 64) { - respond(400, "Invalid public key hash: " . htmlspecialchars($pubkey_hash)); - } - - } else { - respond(400, "Missing public key or public key hash"); - } - - // Retrieve data by public key hash - $stmt = $db->prepare("SELECT * FROM storage WHERE pubkey_hash = :pubkey_hash"); - $stmt->execute([':pubkey_hash' => $pubkey_hash]); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - - if (!$result) { - respond(404, "No data found for this public key"); - } - - // Set the correct MIME type and output the data without encoding - header('Content-Type: ' . $result['mime_type']); - echo $result['data']; - exit; -} - - // Update stored data with a signed transaction - if ($_SERVER['REQUEST_METHOD'] === 'PUT' && $_GET['action'] === 'update') { - parse_str(file_get_contents("php://input"), $_PUT); - - $pubkey = $_PUT['pubkey'] ?? null; - $signature = $_PUT['signature'] ?? null; - $data = $_PUT['data'] ?? null; - $mime_type = $_PUT['mime_type'] ?? 'text/plain'; - - if (!$pubkey || !$signature || !$data) { - respond(400, "Invalid input: missing public key, signature, or data"); - } - - // Decode base64-encoded public key and signature using safe base64 decoding - $decodedPubkey = safeBase64Decode($pubkey); - $decodedSignature = safeBase64Decode($signature); - - if ($decodedPubkey === false || strlen($decodedPubkey) !== 32) { - respond(400, "Invalid public key"); - } - - if ($decodedSignature === false) { - respond(400, "Invalid signature: not valid base64"); - } - - $pubkey_hash = bin2hex(sodium_crypto_generichash($decodedPubkey)); - - if (!sodium_crypto_sign_verify_detached($decodedSignature, $data, $decodedPubkey)) { - respond(400, "Invalid signature"); - } - - $stmt = $db->prepare("SELECT * FROM storage WHERE pubkey_hash = :pubkey_hash"); - $stmt->execute([':pubkey_hash' => $pubkey_hash]); - if (!$stmt->fetch(PDO::FETCH_ASSOC)) { - respond(404, "Space not found"); - } - - if (strlen($data) > 1024) { - respond(400, "Data exceeds 1k size limit"); - } - - $stmt = $db->prepare("UPDATE storage SET data = :data, mime_type = :mime_type WHERE pubkey_hash = :pubkey_hash"); - $stmt->execute([':data' => $data, ':mime_type' => $mime_type, ':pubkey_hash' => $pubkey_hash]); - - respond(200, "Storage updated"); - } - - // Handle deletion (not in spec, but helpful) - if ($_SERVER['REQUEST_METHOD'] === 'DELETE' && $_GET['action'] === 'delete') { - $pubkey = $_POST['pubkey'] ?? null; - $signature = $_POST['signature'] ?? null; - - if (!$pubkey || !$signature) { - respond(400, "Invalid input: missing public key or signature"); - } - - // Decode base64-encoded public key and signature using safe base64 decoding - $decodedPubkey = safeBase64Decode($pubkey); - $decodedSignature = safeBase64Decode($signature); - - if ($decodedPubkey === false || strlen($decodedPubkey) !== 32) { - respond(400, "Invalid public key"); - } - - if ($decodedSignature === false) { - respond(400, "Invalid signature: not valid base64"); - } - - $pubkey_hash = bin2hex(sodium_crypto_generichash($decodedPubkey)); - - if (!CryptoSign::verify_detached($decodedSignature, $pubkey_hash, $decodedPubkey)) { - respond(400, "Invalid signature"); - } - - $stmt = $db->prepare("DELETE FROM storage WHERE pubkey_hash = :pubkey_hash"); - $stmt->execute([':pubkey_hash' => $pubkey_hash]); - - respond(200, "Storage deleted"); - } -} else { - // If no action is specified, show the contents of main.php - if (file_exists('main.php')) { - include 'main.php'; - } else { - respond(400, "No action specified and main.php not found"); - } -} - +exec($query); + + // Check if the 'public_key' column already exists; if not, add it. + $columns = $db->query("PRAGMA table_info(storage)")->fetchAll(PDO::FETCH_ASSOC); + $hasPublicKeyColumn = false; + + foreach ($columns as $column) { + if ($column['name'] === 'public_key') { + $hasPublicKeyColumn = true; + break; + } + } + + if (!$hasPublicKeyColumn) { + $db->exec("ALTER TABLE storage ADD COLUMN public_key BLOB NOT NULL"); + } +} + +// Initialize SQLite and auto-create the database and table if they don't exist +$db = new PDO('sqlite:/home/retrodig/1024clubdb/storage.db'); +initializeDatabase($db); + +// Helper function to respond with JSON +function respond($status, $message, $data = []) { + http_response_code($status); + echo json_encode(['message' => $message, 'data' => $data]); + exit; +} + + + + +/** + * Try to decode a Base64 or URL-safe Base64 string (or data: URI). + * Returns decoded bytes on success, or false on failure. + */ +function maybeDecodeBase64($input) { + if ($input === null) return false; + $trim = preg_replace('/\s+/', '', $input); + if (stripos($trim, 'data:') === 0 && ($pos = stripos($trim, ';base64,')) !== false) { + $payload = substr($trim, $pos + 8); + } else { + $payload = $trim; + } + $payload = strtr($payload, '-_', '+/'); + $pad = strlen($payload) % 4; + if ($pad) { $payload .= str_repeat('=', 4 - $pad); } + $decoded = base64_decode($payload, true); + if ($decoded === false) { return false; } + + // Heuristic to reduce false positives: compare normalized encoding or check for binary-ish data + $re = rtrim(strtr(base64_encode($decoded), '+/', '-_'), '='); + $pa = rtrim(str_replace('=', '', $payload), '='); + if ($re !== $pa) { + $nonPrintable = preg_match_all('/[^\x09\x0A\x0D\x20-\x7E]/', $decoded); + if ($nonPrintable < max(1, (int)(strlen($decoded) * 0.1))) { + return false; + } + } + return $decoded; +} + +// Enhance Base64 decoding to handle both raw and URL-safe inputs +function safeBase64Decode($input) { + // First, attempt a direct Base64 decode + $decoded = base64_decode($input, true); + if ($decoded !== false && strlen($decoded) === 32) { + return $decoded; + } + + // If the direct decode fails, attempt a URL-safe decode + $urlSafeInput = str_replace(['-', '_'], ['+', '/'], $input); + return base64_decode($urlSafeInput, true); +} + +// Handle Base64 encoding safely +function safeBase64Encode($input) { + return str_replace(['+', '/'], ['-', '_'], base64_encode($input)); +} + +// Check if 'action' is set in the query parameters before proceeding +if (isset($_GET['action'])) { + // Create or get storage space based on pubkey hash + if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_GET['action'] === 'create') { + $pubkey = $_POST['pubkey'] ?? null; + + if (!$pubkey) { + respond(400, "Missing public key"); + } + + // Decode the public key, try both raw and URL-safe Base64 decodings + $decodedPubkey = safeBase64Decode($pubkey); + if ($decodedPubkey === false || strlen($decodedPubkey) !== 32) { + respond(400, "Invalid public key"); + } + + $pubkey_hash = bin2hex(sodium_crypto_generichash($decodedPubkey)); + + $stmt = $db->prepare("SELECT * FROM storage WHERE pubkey_hash = :pubkey_hash"); + $stmt->execute([':pubkey_hash' => $pubkey_hash]); + $existing = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($existing) { + respond(200, "Space already exists", ['pubkey_hash' => $pubkey_hash]); + } + + $stmt = $db->prepare("INSERT INTO storage (pubkey_hash, data, mime_type, public_key) VALUES (:pubkey_hash, '', 'text/plain', :public_key)"); + $stmt->execute([':pubkey_hash' => $pubkey_hash, ':public_key' => $decodedPubkey]); + + respond(201, "Space created", ['pubkey_hash' => $pubkey_hash]); + } + +// Retrieve stored data using the public key or public key hash +if ($_SERVER['REQUEST_METHOD'] === 'GET' && $_GET['action'] === 'retrieve') { + $pubkey = $_GET['pubkey'] ?? null; + $pubkey_hash = $_GET['pubkey_hash'] ?? null; + + if ($pubkey) { + // Replace spaces with '+' to account for unencoded URLs + $pubkey = str_replace(' ', '+', $pubkey); + + // Attempt to decode public key (try both standard and URL-safe Base64) + $decodedPubkey = safeBase64Decode($pubkey); + + if ($decodedPubkey === false || strlen($decodedPubkey) !== 32) { + respond(400, "Failed to decode public key: " . htmlspecialchars($pubkey)); + } + + // Hash the decoded public key + $pubkey_hash = bin2hex(sodium_crypto_generichash($decodedPubkey)); + + } elseif ($pubkey_hash) { + // Validate the provided public key hash (must be 64 characters long, hex format) + if (!ctype_xdigit($pubkey_hash) || strlen($pubkey_hash) !== 64) { + respond(400, "Invalid public key hash: " . htmlspecialchars($pubkey_hash)); + } + + } else { + respond(400, "Missing public key or public key hash"); + } + + // Retrieve data by public key hash + $stmt = $db->prepare("SELECT * FROM storage WHERE pubkey_hash = :pubkey_hash"); + $stmt->execute([':pubkey_hash' => $pubkey_hash]); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$result) { + respond(404, "No data found for this public key"); + } + + // Set the correct MIME type and output the data without encoding + header('Content-Type: ' . $result['mime_type']); + echo $result['data']; + exit; +} + + // Update stored data with a signed transaction + if ($_SERVER['REQUEST_METHOD'] === 'PUT' && $_GET['action'] === 'update') { + parse_str(file_get_contents("php://input"), $_PUT); + + $pubkey = $_PUT['pubkey'] ?? null; + $signature = $_PUT['signature'] ?? null; + $data = $_PUT['data'] ?? null; + $mime_type = $_PUT['mime_type'] ?? 'text/plain'; + + if (!$pubkey || !$signature || !$data) { + respond(400, "Invalid input: missing public key, signature, or data"); + } + + // Decode base64-encoded public key and signature using safe base64 decoding + $decodedPubkey = safeBase64Decode($pubkey); + $decodedSignature = safeBase64Decode($signature); + + if ($decodedPubkey === false || strlen($decodedPubkey) !== 32) { + respond(400, "Invalid public key"); + } + + if ($decodedSignature === false) { + respond(400, "Invalid signature: not valid base64"); + } + + $pubkey_hash = bin2hex(sodium_crypto_generichash($decodedPubkey)); + + + // If data looks like base64 (or data:...;base64,...) decode it for size check and storage. + $decodedData = maybeDecodeBase64($data); + $isBase64Payload = ($decodedData !== false); + + // Verify signature. First try verifying exactly what we received (backwards compatibility). + $sigOk = sodium_crypto_sign_verify_detached($decodedSignature, $data, $decodedPubkey); + + // If that fails and it looked like base64, also try verifying the decoded bytes (clients that sign raw bytes). + if (!$sigOk && $isBase64Payload) { + $sigOk = sodium_crypto_sign_verify_detached($decodedSignature, $decodedData, $decodedPubkey); + } + if (!$sigOk) { + respond(403, "Invalid signature"); + } + + // Enforce 1k limit against decoded bytes when base64 was used; otherwise against the raw payload. + $dataForLimit = $isBase64Payload ? $decodedData : $data; + if (strlen($dataForLimit) > 1024) { + respond(400, "Data exceeds 1k size limit"); + } + + // Choose what we store: decoded bytes when base64 was used (so retrieval returns raw), otherwise raw text. + $dataToStore = $isBase64Payload ? $decodedData : $data; + // Normalize MIME type for common images if a data URI was provided. + if ($isBase64Payload && isset($_PUT['mime_type']) && stripos($_PUT['mime_type'], 'image/') === 0) { + $mime_type = $_PUT['mime_type']; + } + // Persist + $stmt = $db->prepare("UPDATE storage SET data = :data, mime_type = :mime_type WHERE pubkey_hash = :pubkey_hash"); + $stmt->execute([':data' => $dataToStore, ':mime_type' => $mime_type, ':pubkey_hash' => $pubkey_hash]); + + respond(200, "Storage updated"); + + } + + // Handle deletion (not in spec, but helpful) + if ($_SERVER['REQUEST_METHOD'] === 'DELETE' && $_GET['action'] === 'delete') { + $pubkey = $_POST['pubkey'] ?? null; + $signature = $_POST['signature'] ?? null; + + if (!$pubkey || !$signature) { + respond(400, "Invalid input: missing public key or signature"); + } + + // Decode base64-encoded public key and signature using safe base64 decoding + $decodedPubkey = safeBase64Decode($pubkey); + $decodedSignature = safeBase64Decode($signature); + + if ($decodedPubkey === false || strlen($decodedPubkey) !== 32) { + respond(400, "Invalid public key"); + } + + if ($decodedSignature === false) { + respond(400, "Invalid signature: not valid base64"); + } + + $pubkey_hash = bin2hex(sodium_crypto_generichash($decodedPubkey)); + + if (!CryptoSign::verify_detached($decodedSignature, $pubkey_hash, $decodedPubkey)) { + respond(400, "Invalid signature"); + } + + $stmt = $db->prepare("DELETE FROM storage WHERE pubkey_hash = :pubkey_hash"); + $stmt->execute([':pubkey_hash' => $pubkey_hash]); + + respond(200, "Storage deleted"); + } +} else { + // If no action is specified, show the contents of main.php + if (file_exists('main.php')) { + include 'main.php'; + } else { + respond(400, "No action specified and main.php not found"); + } +} + ?> \ No newline at end of file diff --git a/main.php b/main.php index 3d23381..0e1a48a 100644 --- a/main.php +++ b/main.php @@ -16,7 +16,8 @@ - Home + Source Code + @@ -168,7 +169,7 @@ php 1kb_client.php retrieve public_key.pem