Last active
November 27, 2025 00:16
-
-
Save TheExpertNoob/45a639f58e5872fc2a890b1f9d864f2a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| // tfl-decrypt.php requires PHP 8.5+ | |
| // Single-file upload + decrypt + return JSON | |
| $privateKeyPath = '/var/www/private.pem'; // Configure this to the PEM format Tinfoil private key location (outside webroot). | |
| // Simple HTML form when no POST | |
| if ($_SERVER['REQUEST_METHOD'] !== 'POST') { | |
| echo '<!doctype html><html><head><meta charset="utf-8"><title>TFL -> JSON</title><style>body {image-rendering: pixelated; background: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyBvcGFjaXR5PSIwLjIiIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDgwMCA4MDAiIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PGZpbHRlciBpZD0nbm9pc2VGaWx0ZXInPjxmZVR1cmJ1bGVuY2UgdHlwZT0nZnJhY3RhbE5vaXNlJyBiYXNlRnJlcXVlbmN5PScwLjY1JyBudW1PY3RhdmVzPSczJyBzdGl0Y2hUaWxlcz0nc3RpdGNoJyAvPjwvZmlsdGVyPjxyZWN0IHdpZHRoPScxMDAlJyBoZWlnaHQ9JzEwMCUnIGZpbHRlcj0ndXJsKCNub2lzZUZpbHRlciknIC8+PC9zdmc+"),linear-gradient(112.1deg, rgb(16, 19, 28) 11.4%, rgb(32, 38, 60) 70.2%); color: white; font-family: "Noto Sans", sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100dvh; margin: 0; padding: 20px; box-sizing: border-box;} .container {text-align: center; max-width: 90vw; position: relative;} .logo {position: absolute; top: -200px; left: 50%; transform: translateX(-50%); max-width: 300px; height: auto;} h1 {font-size: clamp(1.5rem, 5vw, 3rem); margin-bottom: 2rem;} input[type="file"] {font-size: clamp(0.9rem, 2.5vw, 1.1rem); border: 2px solid white; border-radius: 4px;} button {font-size: clamp(0.9rem, 2.5vw, 1.1rem); padding: 10px 20px; cursor: pointer;} span {font-size: clamp(0.8rem, 2vw, 1rem);}</style></head><body><div class="container">'; | |
| echo '<img src="tinfoil.png" alt="Logo" class="logo">'; | |
| echo '<h1>Tinfoil Index<br>Decryption & Decompressor</h1>'; | |
| echo '<form method="post" enctype="multipart/form-data">'; | |
| echo '<input type="file" name="tflfile" accept=".tfl" required> '; | |
| echo '<button type="submit">Upload</button> '; | |
| echo '<br><span>(size limit 200M)</span>'; | |
| echo '</form></div></body></html>'; | |
| exit; | |
| } | |
| if ($_FILES['tflfile']['size'] > 200 * 1024 * 1024) { | |
| http_response_code(400); | |
| echo "File too large; limit is 200MB."; | |
| exit; | |
| } | |
| // Handle upload | |
| if (!isset($_FILES['tflfile']) || $_FILES['tflfile']['error'] !== UPLOAD_ERR_OK) { | |
| http_response_code(400); | |
| echo "Upload failed."; | |
| exit; | |
| } | |
| $tmp = $_FILES['tflfile']['tmp_name']; | |
| $raw = file_get_contents($tmp); | |
| if ($raw === false) { | |
| http_response_code(500); | |
| echo "Failed to read uploaded file."; | |
| exit; | |
| } | |
| // Minimal length check (header + session key + index size fields) | |
| if (strlen($raw) < 0x110) { | |
| http_response_code(400); | |
| echo "File too short to be a valid TFL."; | |
| exit; | |
| } | |
| // 1) Magic check: 7 bytes at offset 0 | |
| $magic = substr($raw, 0, 7); | |
| if ($magic !== "TINFOIL") { | |
| http_response_code(400); | |
| echo "Bad magic; not a TINFOIL .tfl file."; | |
| exit; | |
| } | |
| // 2) Index Type byte at 0x7 | |
| $indexType = ord($raw[0x7]); | |
| // Encryption: session key present at 0x8 length 0x100 (256 bytes) | |
| $sessionKeyEnc = substr($raw, 0x8, 0x100); | |
| // 3) Unencrypted Index Size: 8 bytes at 0x108 (little-endian 64-bit) | |
| $sizeBytes = substr($raw, 0x108, 8); | |
| if (strlen($sizeBytes) !== 8) { | |
| http_response_code(400); | |
| echo "Index size field missing."; | |
| exit; | |
| } | |
| // Little-endian 64-bit unpack: use two 32-bit parts to avoid platform issues | |
| $parts = array_values(unpack('Vlo/Vhi', $sizeBytes)); // lo, hi | |
| $indexSize = $parts[0] + ($parts[1] << 32); | |
| // Determine flags: encryption is high nibble, compression is low nibble | |
| $isEncrypted = (($indexType & 0xF0) === 0xF0); | |
| $compFlag = $indexType & 0x0F; // 0x0D = zstd, 0x0E = zlib, 0x00 = uncompressed | |
| $plainIndex = null; | |
| if ($isEncrypted) { | |
| // Load private key | |
| if (!file_exists($privateKeyPath)) { | |
| http_response_code(500); | |
| echo "Server private key not found (configured path: $privateKeyPath)."; | |
| exit; | |
| } | |
| $privKeyPem = file_get_contents($privateKeyPath); | |
| $privateKey = openssl_pkey_get_private($privKeyPem); | |
| if ($privateKey === false) { | |
| http_response_code(500); | |
| echo "Failed to load private key. Check permissions and that the file is a valid PEM private key."; | |
| exit; | |
| } | |
| $ciphertext_full = substr($raw, 0x110); | |
| $ciphertext_len = strlen($ciphertext_full); | |
| // Expected padded ciphertext length (multiple of 16) | |
| $expected_cipher_len = (($indexSize + 15) >> 4) << 4; // round up | |
| // --- RSA unwrap --- | |
| $aesKey = null; | |
| // Try OAEP with SHA-256 (PHP 8.5+ supports this) | |
| $ok = openssl_private_decrypt($sessionKeyEnc, $aesKey, $privateKey, OPENSSL_PKCS1_OAEP_PADDING, 'SHA256'); | |
| if (!$ok || $aesKey === null) { | |
| echo "<pre>"; | |
| echo "RSA unwrap failed or returned null key.\n"; | |
| echo "openssl_error_string(): " . openssl_error_string() . "\n"; | |
| echo "</pre>"; | |
| http_response_code(500); | |
| exit; | |
| } | |
| // --- AES decrypt --- | |
| // AES-128-ECB with zero padding | |
| $decrypted = openssl_decrypt($ciphertext_full, 'aes-128-ecb', $aesKey, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING); | |
| if ($decrypted === false) { | |
| echo "<pre>"; | |
| echo "AES decryption failed (wrong key/padding?)\n"; | |
| echo "OpenSSL error: " . openssl_error_string() . "\n"; | |
| echo "</pre>"; | |
| http_response_code(500); | |
| exit; | |
| } | |
| // Trim null padding and cut to declared plaintext size | |
| $unpadded = rtrim($decrypted, "\x00"); | |
| $plainIndex = substr($unpadded, 0, $indexSize); | |
| } else { | |
| // Not encrypted — just take raw index data | |
| $plainIndex = substr($raw, 0x110, $indexSize); | |
| } | |
| // Now decompress if needed | |
| $finalJson = $plainIndex; | |
| if ($compFlag === 0x0D) { | |
| // Zstandard | |
| // Prefer PHP zstd extension if available, otherwise try zstd CLI | |
| if (function_exists('zstd_uncompress')) { | |
| $out = @zstd_uncompress($plainIndex); | |
| if ($out === false) { | |
| http_response_code(500); | |
| echo "zstd_uncompress failed (extension returned error)."; | |
| exit; | |
| } | |
| $finalJson = $out; | |
| } else { | |
| // try CLI fallback: create temp file, shell out 'zstd -d -c' | |
| $tmpIn = tempnam(sys_get_temp_dir(), 'tfl_zst_in_'); | |
| $cmd = null; | |
| if ($tmpIn === false) { | |
| http_response_code(500); | |
| echo "Failed to create temp file for zstd fallback."; | |
| exit; | |
| } | |
| file_put_contents($tmpIn, $plainIndex); | |
| // check if zstd exists | |
| $which = shell_exec('which zstd 2>/dev/null'); | |
| if ($which) { | |
| $which = trim($which); | |
| $cmd = escapeshellcmd($which) . ' -d -c ' . escapeshellarg($tmpIn) . ' 2>/dev/null'; | |
| $out = shell_exec($cmd); | |
| unlink($tmpIn); | |
| if ($out === null || $out === '') { | |
| http_response_code(500); | |
| echo "zstd CLI decompression failed or produced empty output. Ensure zstd is installed and the data is valid."; | |
| exit; | |
| } | |
| $finalJson = $out; | |
| } else { | |
| unlink($tmpIn); | |
| http_response_code(500); | |
| echo "Zstandard decompression requested but no zstd PHP extension or zstd CLI found on server."; | |
| exit; | |
| } | |
| } | |
| } elseif ($compFlag === 0x0E) { | |
| // zlib (zlib-compressed) | |
| $out = @gzuncompress($plainIndex); | |
| if ($out === false) { | |
| // try gzdecode (gzip) as fallback | |
| $out2 = @gzdecode($plainIndex); | |
| if ($out2 === false) { | |
| http_response_code(500); | |
| echo "zlib decompression failed."; | |
| exit; | |
| } else { | |
| $finalJson = $out2; | |
| } | |
| } else { | |
| $finalJson = $out; | |
| } | |
| } else { | |
| // uncompressed: finalJson already set to plainIndex | |
| } | |
| // Validate JSON (optional but useful) | |
| $maybeJson = json_decode($finalJson, true); | |
| if ($maybeJson === null && json_last_error() !== JSON_ERROR_NONE) { | |
| // Not valid JSON: we can still return the content, but warn | |
| // Return as file but with notice in headers | |
| header('X-TFL-Parse-Warning: invalid-json'); | |
| } | |
| // Send as download | |
| header('Content-Type: application/json'); | |
| header('Content-Disposition: attachment; filename="index.json"'); | |
| header('Content-Length: ' . strlen($finalJson)); | |
| echo $finalJson; | |
| exit; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment