Httpd – decrypting AES files in an apache module

apache-2.2encryptionhttpd

I have a client with a security policy compliance requirement to encrypt certain files on disk. The obvious way to do this is with Device-mapper and an AES crypto module However the current system that is already in place is setup to generate individual files that are encrypted.

Basically they have an apache server using SSL and basic/digest style authentication, and they need to decrypt encrypted files from AES on disk, before re-encrypting them with an SSL. (obviously I can use mod_php, mod_perl, however the idea was to keep static files only on this box)

Do I have any options for decrypting files on-the-fly in apache?

I see that mod_ssl and mod_session_crypto do encryption/decryption or something similar but not exactly what I am after as they are encryption on-the-wire and I am looking for encryption on-disk.

I could imagine that a PerlSetOutputFilter would work with a suitable Perl script configured, and I also see mod_ext_filter so I could just fork a unix command and decrypt the file, but they both feel like a hack.

I am kind of surprised that there is no mod_crypto available…or am I missing something obvious here?

Presumably resource-wise the perl filter is the way to go?

Best Answer

This is a facinating problem. I've been in similar situations where I was required to solve a problem the wrong way. That being said, I'm not aware of a general purpose crypto module that will let you decrypt static files as they are served. I searched for several hours but I wasn't able to find anything. That's not surprising as it seems like a fairly specialized problem.

Nonetheless, I see several options.

  1. Write your own C module and implement it as a filter handler.
  2. Use mod_ext_filter and an external executable1 to decrypt the file.
  3. If all of the files have a specific extension, add a handler to call a CGI script to decrypt and serve the file. For example, Action decrypt /cgi-bin/decrypt.pl and AddHandler decrypt .html will cause all requests for files ending in .html to call /cgi-bin/decrypt.pl which will need to read, decrypt, and serve the file. See http://httpd.apache.org/docs/current/handler.html#examples.
  4. Use mod_php and mod_rewrite to transparently decrypt and serve static files of any type from one or more specific locations.

I took the liberty of implementing a proof of concept of the fourth option which I'll share here. I chose PHP in this instance because it's ubiquitous, it usually contains the necessary crypto functions, and it's fast since it's an Apache module written in C.

Assuming that all encrypted files are stored in a directory called data and are encrypted using the same key, I created the directory in my document root then created an encrypted file using OpenSSL.

cd /var/www
mkdir data
echo 'This is my plaintext content.' | openssl aes-256-cbc -a -k secret -out data/test.txt

Then I created decrypt.php in my document root with the following content:

<?php
# NOTE: This is proof-of-concept code. You should audit it for security before
# using it in a production environment.

# The key to use to decrypt the files
$key = 'secret';

$filename  = $_SERVER["DOCUMENT_ROOT"] . htmlspecialchars($_SERVER['REQUEST_URI']);
$data      = file_get_contents($filename);
$plaintext = decrypt($key, $data);

# Determine the MIME type of the decrypted content
$finfo        = finfo_open(FILEINFO_MIME_TYPE);
$content_type = finfo_buffer($finfo, $plaintext);
finfo_close($finfo);

header("Content-type: $content_type");
print $plaintext;

function decrypt($password, $encrypted_data) {
  $encrypted_data = base64_decode($encrypted_data);
  $salt           = substr($encrypted_data, 8, 8); 
  $cyphertext     = substr($encrypted_data, 16);

  $password    = $password . $salt;
  $md5_hash    = array();
  $md5_hash[0] = md5($password, true);
  $result      = $md5_hash[0];

  $rounds = 3;
  for ($i = 1; $i < $rounds; $i++) {
      $md5_hash[$i] = md5($md5_hash[$i - 1] . $password, true);
      $result .= $md5_hash[$i];
  }

  $key = substr($result, 0, 32);
  $iv  = substr($result, 32,16);

  return openssl_decrypt($cyphertext, 'aes-256-cbc', $key, true, $iv);
}
?>

If your content is not base64 encoded then you can remove the first line in the decrypt function which reads $encrypted_data = base64_decode($encrypted_data);.

Finally, in my Apache configuration I added the following entries:

<directory /var/www/>
    RewriteEngine on
    RewriteCond %{REQUEST_FILENAME}    -f
    RewriteRule ^data/                 /decrypt.php  [L]
</directory>

Now any file requested from http://www.example.com/data will be decrypted and served, including from within subdirectories. Since the rewrite condition is limiting the rewrite to files, directory indexes still work. I have the PHP script determine the MIME type of the decrypted content and update the header accordingly so I can serve encrypted images, documents, web pages, and so on.

This probably won't be as fast as a custom Apache module, but it should be close. If you're going to be serving a lot of encrypted content and performance is an issue, you might be able to speed things up a little bit by installing the Alternative PHP Cache so the PHP page doesn't have to be compiled for every request.

Please keep in mind that you may need to increase the settings for PHP's memory usage if you are decrypting large files.


1 You said that you feel like this option is a hack. Why? Although the result may be slow, mod_ext_filter is a supported core module and will do the job once you write a filter program for it to use.

2 I borrowed the decrypt() function from http://us3.php.net/manual/en/function.openssl-decrypt.php#107210.