Greetings fellow devs.
Please note that some information has to be withheld due to privacy and NDA.
We have a client that uses a PGP Public Key that I shared with them to encrypt GDPR sensitive data and in return, they send us the encrypted file. The idea is to allow our direct reports to upload the encrypted file via an HTML/AJAX/PHP SPA that does various validation checks then decrypts the file and finally allows the user to download the decrypted file.
As expected, decrypting the file via terminal works flawlessly with a command such as:
gpg --decrypt encrypted_file.ext > decrypted_file.ext
As you’re all well aware, it’s at this point you’d be prompted for the passphrase which, once entered, would output the decrypted file flawlessly.
Now, I created an SPA web interface that allows the encrypted file to be uploaded, undergoes decryption, and upon the AJAX response, an object is returned containing the path to a file named download.php
which receives a URL parameter which is an encoded string containing both encrypted and decrypted filenames.
No matter what I have tried, the common error is that the decrypted version of the file has a zero length and obviously means the decryption process failed.
I have tried various approaches with my latest being using inotify-tools
to watch the upload target directory to do the following:
-
chown the encrypted file from www-data to the system user used to create the keys;
-
run the following command to decrypt the uploaded file:
"echo 'passphrase' | gpg --batch --quiet --yes --passphrase-fd 0 --decrypt $encryptedFilename > " . $decryptedFilename;
-
then chown the decrypted file from system user to www-data for safe download, and finally;
-
the
download.php
would make use of theContent-Disposition
header to force the decrypted file download;
The decrypted file always has zero length as I mentioned before meaning the decryption failed. Unfortunately, there are no errors to speak of and hence, my reaching out for any suggestions.
Here is my upload validation class to manage the usual upload irritations:
<?php
class UploadValidationProd
{
protected $uploadedForm;
protected $buttonName;
protected $uploadedFile;
protected $inputFileName;
protected $maximumFileSize;
protected $validated;
/**
* @param $post
* @param $buttonName
* @param $files
* @param $inputFileName
*/
public function __construct($post, $buttonName, $files, $inputFileName)
{
$this->uploadedForm = $post;
$this->buttonName = $buttonName;
$this->uploadedFile = $files;
$this->inputFileName = $inputFileName;
$this->maximumFileSize = 20000000;
$this->validated = FALSE;
}
/**
* @return bool
*/
public function CheckFormPosted(): bool
{
if (isset($this->uploadedForm[$this->buttonName])) {
return TRUE;
}
return FALSE;
}
/**
* @return bool
*/
public function CheckFilePosted(): bool
{
if (!empty($this->uploadedFile[$this->inputFileName]['name'])) {
return TRUE;
}
return FALSE;
}
/**
* @return bool
*/
public function CheckFileTypeAllowed(): bool
{
$allowedTypes = [
'csv',
'xls',
'xlsx'
];
$fileType = strtolower(pathinfo($this->uploadedFile[$this->inputFileName]['name'], PATHINFO_EXTENSION));
if (!in_array($fileType, $allowedTypes)) {
return FALSE;
}
return TRUE;
}
/**
* @return bool
*/
public function CheckFileSize(): bool
{
if ($this->uploadedFile[$this->inputFileName]['size'] > $this->maximumFileSize) {
return FALSE;
}
return TRUE;
}
/**
* @return array
*/
public function DoValidation(): array
{
$checkFormPosted = $this->CheckFormPosted();
if ($checkFormPosted) {
$checkFilePosted = $this->CheckFilePosted();
$checkFileTypeAllowed = $this->CheckFileTypeAllowed();
$checkFileSize = $this->CheckFileSize();
if (!$checkFilePosted) {
return [
'result' => 'error',
'message' => 'No file was selected'
];
}
if (!$checkFileTypeAllowed) {
return [
'result' => 'error',
'message' => 'The selected file type is not allowed'
];
}
if (!$checkFileSize) {
return [
'result' => 'error',
'message' => 'The selected file size exceeds the maximum allowed size (' . $this->maximumFileSize / 1000000 . 'MB)'
];
}
if (move_uploaded_file($this->uploadedFile[$this->inputFileName]['tmp_name'], $this->uploadedFile[$this->inputFileName]['name'])) {
return [
'result' => 'success',
'message' => 'The selected file was successfully uploaded'
];
}
}
return [
'result' => NULL,
'message' => NULL
];
}
}
Here is the decryption class:
<?php
class UploadDecryptionProd
{
protected $encryptedFile;
protected $decryptedFile;
protected $pgpPassphrase;
/**
* @param $encryptedFile
*/
public function __construct($encryptedFile)
{
$this->encryptedFile = $encryptedFile;
$this->decryptedFile = 'decrypted_' . $encryptedFile;
$this->pgpPassphrase = 'theOriginalPassphrase';
}
/**
* @return array
*/
public function Decrypt(): array
{
$cmd = "echo '$this->pgpPassphrase' | gpg --batch --quiet --yes --passphrase-fd 0 --decrypt $this->encryptedFile > " . $this->decryptedFile;
$result = 0;
system($cmd, $result);
if ($result == '0') {
return [
'result' => 'success',
'message' => $this->decryptedFile
];
} else {
return [
'result' => 'error',
'message' => $this->encryptedFile . ' could not be decrypted'
];
}
}
}
If anyone can point me in the right direction, a helpful hint or maybe even a "Oh hey – you just need to add a couple of eggs and sing to the sun", I would be most obliged.
Thanks very much everyone 🙂
2
Answers
I have finally found a solution that works.
My OS is Raspbian Bullseye (Debian Bullseye basically)
I copied the default user's keyring to Apache's www-data directory:
cp -a ~/.gnupg/. /var/www/.gnupg/
Gave ownership of the copied keyring to www-data:
sudo chown -R www-data:www-data
Changed permissions on the copied keyring:
sudo find /var/www/.gnupg -type f -exec chmod 600 {} ;
sudo find /var/www/.gnupg -type d -exec chmod 700 {} ;
Then finally, this is the PHP code:
And the output on the web page:
I hope this helps someone.
I’m not sure but it looks like a bug:
Maybe