filename = $outName; $gpg = trim((string) @shell_exec('command -v gpg 2>/dev/null')); if ($gpg === '') { $res->error = 'gpg is not installed on this server.'; return $res; } if (!is_readable(KEY_FILE)) { $res->error = 'Private key not found: ' . basename(KEY_FILE); return $res; } if (!is_readable(PASSPHRASE_FILE)) { $res->error = 'Passphrase file not found: ' . basename(PASSPHRASE_FILE) . ' — create it and put the key passphrase inside.'; return $res; } $passphrase = rtrim((string) file_get_contents(PASSPHRASE_FILE), "\r\n"); if ($passphrase === '') { $res->error = 'Passphrase file is empty.'; return $res; } // Isolated, throwaway keyring. $home = sys_get_temp_dir() . '/pgpdec_' . bin2hex(random_bytes(8)); if (!mkdir($home, 0700) && !is_dir($home)) { $res->error = 'Could not create temporary keyring.'; return $res; } try { // 1) Import the private key into the isolated home. $import = run_gpg($gpg, $home, ['--batch', '--quiet', '--import', KEY_FILE], ''); if ($import['code'] !== 0 && stripos($import['stderr'], 'secret key imported') === false) { $res->error = 'Key import failed.'; $res->log = $import['stderr']; return $res; } // 2) Decrypt. Ciphertext is a file argument; passphrase comes via stdin. $dec = run_gpg($gpg, $home, [ '--batch', '--yes', '--quiet', '--pinentry-mode', 'loopback', '--passphrase-fd', '0', '--decrypt', $cipherPath, ], $passphrase, true); if ($dec['code'] !== 0) { $res->error = 'Decryption failed — check the passphrase and that this key can decrypt the file.'; $res->log = $dec['stderr']; return $res; } $res->ok = true; $res->plaintext = $dec['stdout']; $res->log = $dec['stderr']; return $res; } finally { rrmdir($home); } } /** * Invoke gpg with an isolated GNUPGHOME. Passphrase/other input goes to stdin. * @return array{code:int,stdout:string,stderr:string} */ function run_gpg(string $gpg, string $home, array $args, string $stdin, bool $binaryOut = false): array { $cmd = escapeshellarg($gpg); foreach ($args as $a) { $cmd .= ' ' . escapeshellarg($a); } $descriptors = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $env = ['GNUPGHOME' => $home, 'LC_ALL' => 'C', 'PATH' => getenv('PATH') ?: '/usr/bin:/bin:/usr/local/bin']; $proc = proc_open($cmd, $descriptors, $pipes, $home, $env); if (!is_resource($proc)) { return ['code' => 127, 'stdout' => '', 'stderr' => 'Failed to start gpg.']; } fwrite($pipes[0], $stdin); fclose($pipes[0]); $stdout = stream_get_contents($pipes[1]); $stderr = stream_get_contents($pipes[2]); fclose($pipes[1]); fclose($pipes[2]); $code = proc_close($proc); return ['code' => $code, 'stdout' => (string) $stdout, 'stderr' => (string) $stderr]; } /** Recursively remove a directory. */ function rrmdir(string $dir): void { if (!is_dir($dir)) { return; } foreach (scandir($dir) ?: [] as $entry) { if ($entry === '.' || $entry === '..') { continue; } $path = $dir . '/' . $entry; is_dir($path) ? rrmdir($path) : @unlink($path); } @rmdir($dir); } /** Strip a .pgp/.gpg/.asc suffix for the output filename. */ function output_name(string $name): string { $base = basename($name); foreach (['.pgp', '.gpg', '.asc'] as $ext) { if (str_ends_with(strtolower($base), $ext)) { return substr($base, 0, -strlen($ext)); } } return $base . '.decrypted'; } // --------------------------------------------------------------------------- // Request handling // --------------------------------------------------------------------------- $error = ''; $log = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $result = null; // Mode A: uploaded file. if (!empty($_FILES['cipher']['tmp_name']) && is_uploaded_file($_FILES['cipher']['tmp_name'])) { if (($_FILES['cipher']['size'] ?? 0) > MAX_UPLOAD) { $error = 'Uploaded file is too large (max ' . (MAX_UPLOAD / 1024 / 1024) . ' MB).'; } else { $result = decrypt_file( $_FILES['cipher']['tmp_name'], output_name((string) ($_FILES['cipher']['name'] ?? 'upload')) ); } } else { $error = 'Choose a file to decrypt.'; } if ($result !== null) { if ($result->ok) { // Stream the decrypted content as a download. header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . str_replace('"', '', $result->filename) . '"'); header('Content-Length: ' . strlen($result->plaintext)); header('X-Content-Type-Options: nosniff'); echo $result->plaintext; exit; } $error = $result->error; $log = $result->log; } } ?> PGP Decryptor

🔓 PGP Decryptor

Decrypts a PGP-encrypted attachment using the predefined private key stored on the server (). The key is passphrase-protected; the passphrase is read from a separate file and is never displayed or sent to the browser. Decryption runs in an isolated, throwaway keyring and the result is streamed back as a download.
gpg output
⚠️ Passphrase file is missing. Create it in this folder and put the private-key passphrase inside before decrypting.

Decrypt an uploaded file

Max MB. The file must be encrypted to the key held on this server.

Keep and out of the web root or blocked from direct access (see the bundled .htaccess). Anyone who can reach this page can decrypt files with this key.