Lock Down Encryption with PHP

7 min read

Table of Contents:

Title Image for the Article

Create an encryption and decryption app to share files securely across the internet. Instead of sending your files to the mercy of an email client, run a server that keeps information to those who need it.

Introduction

This project is using vanilla PHP. No frameworks, Composer, Laravel, or any of that fancy business. Instead, this time we will show what can be done with PHP by itself. The finished code is linked below on Github

cobb208/phpencryptionapp (github.com)

Project Setup

If you have not setup a project before in PHP don't worry! Look at the options below, this project was made in PHP8.1. No big features were used from the 8.X library so you should be safe with 7.X if you do not want to upgrade just yet.

Option 1 (Easiest All Computers)

If you are brand new or do not feel comfortable with the command line, yet I would recommend XAMPP to run a virtual machine for you. XAMPP runs a modified Linux distribution that is designed to run a webserver on any operating system.

A simple read of the documentation will show you exactly where you need to put your files. It even installs PHP for you!

Option 2 (Windows)

Utilize Windows Subsystem for Linux to run a webserver inside of your Windows instance. This option will keep you in the familiar land of Windows but allow you to use the command line with an actual production Linux Distribution.

This option is a bit more advanced as you will need to:

  • Install Apache Webserver
  • Install MySQL
  • Learn how to start up services
  • Learn the small differences between Linux and WSL Linux

I recommend this option for someone looking to learn backend web development on a Windows machine.

Option 3 (Mac)

Utilize Homebrew on your Mac to setup the server. Homebrew works like a package manager in other Unix distributions. For my Mac this is what I choose. It bridges the gap of Mac's Unix and Linux. Follow the documentation on installation steps.

Option 4 (Linux)

In the end, everything you are using is Linux. If you are using a Linux computer, I imagine you are comfortable with the command line and what will be discussed throughout the article. Ensure you have PHP, Apache, and MySQL installed.

IDE or Text Editor

I would recommend using a text editor or IDE when you work with PHP. My favorite text editor for PHP is PHPStorm . It has a lot of powerful tools and helps you work with PHP, HTML, CSS, JavaScript, and databases. However! It costs money... A free and great option is Visual Studio Code.

var/www/html

This is the location on most Linux distributions that Apache will setup its web server. You should have an index.html file in here to test if the website works. If you are using XAMPP spool up your server, other means ensure you start the Apache and MySQL services.

To check if your Apache service is up and running, in your web browser go to http://localhost Apache runs on the localhost alias as well as the machines IP Address. It defaults with port 80. You do not have to put the port information in the URL because web browsers know to ask port 80 first.

We will have three files, index.php, connection.php, decryption.php

Setup the Database

Ensure you have a database setup and a user that can login into a database through localhost. My database will be named "encryptointest" and the single table structure will be:

create table files
(
    id             int auto_increment
        primary key,
    passcode       varchar(255)         not null,
    iv             varbinary(255)       not null,
    filepath       varchar(255)         not null,
    sender_email   varchar(200)         not null,
    receiver_email varchar(100)         not null,
    created_at     datetime             not null,
    picked_up      tinyint(1) default 0 not null,
    tag            varbinary(255)       not null,
    file_ext       varchar(50)          null,
    constraint table_name_id_uindex
        unique (id)
);

This is the setup we will use throughout the program.

Connection.php

This file will hold our connection string to our database. The file will just hold variables to be used in other places. This design is not always the best because you will store a password in plaintext. You can setup Apache rules to forward around this file. They will never be exposed to the internet; however, if the PHP interpreter becomes compromised on your server it can become exposed. ENV variables can solve this problem.

<?php

$sql_hostname = 'localhost';
$sql_username = 'YOUR USERNAME';
$sql_password = 'YOURPASSWORD';
$sql_database = 'encryptiontest';

index.php

The first thing we will do is import the connection file so we can use the variables within it.

<?php
require_once('./connection.php');

The HTML

The HTML used in this program is basic and does not have CSS. It fits the purpose of this program and would love to be styled by you!

<form action="http://localhost/encryption/index.php" method="post" enctype="multipart/form-data">
    <fieldset>
        <legend>Emails</legend>
        <label for="ownerEmail">Your Email:</label>
        <input type="email" id="ownerEmail" name="ownerEmail" required />

        <label for="receiverEmail">Their Email:</label>
        <input type="email" id="receiverEmail" name="receiverEmail" required />
    </fieldset>
    <fieldset>
        <legend>Password</legend>
        <label for="password">Password:</label>
        <input type="password" id="password" name="password" required />
    </fieldset>
    <fieldset>
        <legend>File</legend>
        <label for="uploadFile">File:</label>
        <input type="file" id="uploadFile" name="uploadFile">

        <input type="submit" value="Submit" />
    </fieldset>
</form>


<h1>Retrieve</h1>

<form action="http://localhost/encryption/decryption.php" method="post">
    <fieldset>
        <legend>Emails</legend>
        <label for="yourEmail">Your Email</label>
        <input type="email" name="yourEmail" id="yourEmail" required/>
    </fieldset>
    <fieldset>
        <legend>Passcodes</legend>
        <label for="retrievePassword">Enter the pass phrase</label>
        <input type="password" id="retrievePassword" name="password" required/>
        <input type="submit" value="Retrieve" />
    </fieldset>
</form>

We have two forms, 1. that creates the information and one that requests it.

The Post

The rest of the code will be wrapped in an IF statement to do a simple check to see if it is a POST request.

if(isset($_POST['ownerEmail'])) { ... }

File Setup

$destination_path = getcwd().DIRECTORY_SEPARATOR;

$file_name_arr = explode('.', $_FILES['uploadFile']['name']);
$file_ext = end($file_name_arr);

$file_name = substr(hash('sha256', basename($_FILES['uploadFile']['name'])), 0, 15);

$target_path = $destination_path . 'uploads/' . $file_name;

$file_contents = file_get_contents($_FILES['uploadFile']['tmp_name']);

PHP comes with a lot of helper functions that let us grab directories, expand file names and grab extensions, contract string, and retrieve the file contents. Let's talk about what is happening above in a list.

  1. Grab the current working directory and add the separator / to the end.
  2. Explode will create an array of strings based on the special character given.
  3. End will grab the last member of an array (which will be our file extension).
  4. We will create a file name that is 15 characters long and given a hashed name for uniqueness.
  5. We create a local storage (uploads folder) for our files.
  6. We will then grab the contents from the uploaded file.

The next part will use more hashing and encryption to store the information.

$passcode = hash('sha256', $_POST['password']);
$cipher = 'aes-128-gcm';
$ivlen = openssl_cipher_iv_length($cipher);
$iv = openssl_random_pseudo_bytes($ivlen);

$cipher_text = openssl_encrypt($file_contents, $cipher, $passcode, $options=0, $iv, $tag);

$fp = fopen($target_path, 'w');
fwrite($fp, $cipher_text);
fclose($fp);

chmod($target_path, 0664); // Ensure file cannot execute.
  • We hash the password, so it is stored in a one-way encryption.
  • We grab the cipher method we will use in this project (aes-128-gcm)
  • ivlen and iv creates a unique cipher byte string for encryption
  • cipher_text stores the encrypted information. Pay special attention to $tag it is declared in the function to store special information needed for decryption.
  • The last part opens the file writes the cipher text and closes the file.
  • The last step is to set permissions on the file to read write for our server and just read for anyone else.

The next part is creating the SQL information and using PHP sanitization steps to clean the input.

$sender_email = filter_var($_POST['ownerEmail'], FILTER_SANITIZE_EMAIL);
$receiver_email = filter_var($_POST['receiverEmail'], FILTER_SANITIZE_EMAIL);
$date = new DateTime('now', new DateTimeZone('UTC'));
$created_at = $date->format('Y-m-j G:i:s');
$file_path = $target_path;

if(!isset($sql_hostname) or !isset($sql_username) or !isset($sql_password) or !isset($sql_database)) { die; }
$mysqli = new mysqli($sql_hostname, $sql_username, $sql_password, $sql_database);

$stmt = $mysqli->prepare(
    "INSERT INTO files(passcode, iv, filepath, sender_email, receiver_email, created_at, tag, file_ext)
        VALUES(?, ?, ?, ?, ?, ?, ?, ?)
");


$stmt->bind_param('ssssssss', $passcode, $iv, $file_path, $sender_email, $receiver_email, $created_at, $tag, $file_ext);

$stmt->execute();
  • We use PHP function filter_var to sanitize input, the second input of the function lets it know what kind of input it should expect (and the rules to run).
  • We run an if statement just to ensure all variables are set (from the connections file) if not the program will die.
  • We will then create our MySQLi connection.
  • We will then use a prepare statement to generate our SQL.
  • We will then bind our parameters (the sssssss is telling it should expect strings for all values).
  • We then execute the SQL query and have created our record of the file.

What have we done so far?

Our program takes a file encrypts the information, stores a file, and then creates a record in a SQL database so it can be referenced later.

To test it yourself, open up the index page in your web browser, fill out the top form and submit. It should route you back to this page. If not, check your Apache's error log file for more information at why it might have failed.

decryption.php

Now we will create the file that takes our input, gets the file, decrypts it, and sends it back to us.

<?php
require_once('connection.php');

if(isset($_POST['yourEmail']) and isset($_POST['password'])) { ... rest of the code }

We will be wrapped in another if statement to ensure it is a post request. If someone ran a get request (normal browser request) on this file, it would simply show a blank page

$receiver_email = filter_var($_POST['yourEmail'], FILTER_SANITIZE_EMAIL);
$passphrase = hash('sha256', $_POST['password']);

if(!isset($sql_hostname) or !isset($sql_username) or !isset($sql_password) or !isset($sql_database)) { die; }
$mysqli = new mysqli($sql_hostname, $sql_username, $sql_password, $sql_database);

$sql =
    "SELECT iv, filepath, tag FROM files WHERE passcode = '" . $passphrase . "' AND receiver_email = '" . $receiver_email . "' LIMIT 1";


$result = $mysqli->query($sql);

$result_filepath = '';
$result_iv = '';
$result_tag = '';

if($result->num_rows > 0)
{
    while($row = $result->fetch_assoc())
    {
        $result_filepath = $row['filepath'];
        $result_iv = $row['iv'];
        $result_tag = $row['tag'];
    }
}

This block of code does a lot of similar steps from the previous file so we will keep it short. The main thing to look at is the while loop. Even though the SQL is a limit 1 (only 1 item will be returned) it will still return an array. That while loop will only run once but it is required.

$cipher = 'aes-128-gcm';

$file_contents = file_get_contents($result_filepath);

$plain_text = openssl_decrypt($file_contents, $cipher, $passphrase, $options=0, $result_iv, $result_tag);

$file_prefix = explode("@", $receiver_email);

$date = new DateTime('now', new DateTimeZone('UTC'));
$date_stamp = $date->format('is');

$tmp_file = fopen('/tmp/' . $file_prefix[0] . $date_stamp . '.txt', 'w');
$true_file_name = $file_prefix[0] . $date_stamp . '.txt';

fwrite($tmp_file, $plain_text);

fclose($tmp_file);

$file_name = basename($tmp_file['name']);

header('Content-Type: multi-part/form-data');
header("Content-Disposition: attachment; filename=$true_file_name");

readfile('/tmp/' . $true_file_name);
unlink('/tmp/' . $true_file_name);


exit;

Now we get to do the opposite of encryption. All the data values stored prior are now used to help us decrypt the information. Once we have received the decrypted information. We will create the file with a special unique name for the user. Instead of just giving a hashed value as a name, the name will seem a bit more personalized showing their first part of their email (and the minutes/seconds to keep it unique).

Once the file is created, we will use headers to let the browser know what to expect. The content type is set, and the file name is given so the browser has a safe hand off of information.

The last part is to unlink, remove, the decrypted file, from our server. Only secure information is stored on the server.

What Now?

Take this application and improve it. This is a concept and needs more refinement before deployment. It functions but needs error handling,

Recent Articles

Push Weight with Python

Create a simple terminal application that can let you know how much weight you need to put on the ba...

Validate Forms in TypeScript

Relying on HTML to validate your forms is just one step in ensuring visitors enter their information...

Automation with NodeJS

NodeJS can help you automate simple tasks. Being a web developer you can achieve the same goals as o...

Automation with Python

Python can take boring tasks away from you so you can continue to do other things. It is a scripting...

Speed With Cache in WordPress

Most WordPress sites use Apache as their server. Apache has a lot of tools built into it to speed up...

Social Media