Skip to content

Instantly share code, notes, and snippets.

@TheExpertNoob
Last active November 27, 2025 00:16
Show Gist options
  • Select an option

  • Save TheExpertNoob/45a639f58e5872fc2a890b1f9d864f2a to your computer and use it in GitHub Desktop.

Select an option

Save TheExpertNoob/45a639f58e5872fc2a890b1f9d864f2a to your computer and use it in GitHub Desktop.
<?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