How to improve privacy with Laravel file encryption
Tim Geisendörfer • March 11, 2022
laravel privacy encryptionIf you want to store or process sensible user data. It might make sense to encrypt the files on your storage system to improve user privacy. I am going to prove that regarding a simple use case without using any external packages. You can do that easily with Laravel on board tools.
Why do we need encryption ?
If you are here for the "let's do some code" part you can skip this paragraph. But you will miss some interesting information.
As privacy criteria become more and more in focus, it makes sense to design your web applications from the ground up with this in mind. Especially when you want to process user sensitive data. We Europeans have one of the strictest data protection laws in the world called GDPR. And you should try to comply with it, otherwise it can have serious consequences. This article should be interesting for people all over the world not only Europeans.
Encrypting sensitive data is one of the key elements to strength your privacy. As always the Laravel framework does its job and makes it easy to encrypt database columns with the encrypted casting feature. With it, you can encrypt your model attributes. So any potential attacker who gets access to your database cannot do anything with this information. The whole database will be useless without having the decryption key. But can we achieve some similar improvements to files uploaded by our users?
If you look into your application you will notice that your encryption key sits behind the APP_KEY entry in the .env configuration file. And if you are using the Local storage driver all of your files lie on the same filesystem one cd command away. In this case encrypting your files is completely nonsense. It only makes sense to encrypt your files when they are lying on a remote storage or when you store the encryption keys at another place.
In most of the cases this will be some AWS S3 compatible storage. And when you want to use remote storage encrypting files completely makes sense.
You never can be sure what your storage or server infrastructure provider will do with your data.
In general, external security measures that cannot be influenced should always be viewed critically.
So adding another level of security with encrypting your data at application level always helps to improve your privacy.
Besides, that every major Cloud provider is a US based company and falls under the CLOUD Act. Encrypting your data might be the only option to use these services legally for European based companies.
On top of that the Laravel encryption uses digital signatures and a MAC to verify that your data was not compromised by anyone. With these signatures you always can be sure that your application is the only instance who added or modified your data. This can be really helpful if you want to transfer data through other 3rd party systems and make sure that nobody manipulated your data.
Code examples
In this article we won't use any 3rd party composer package to achieve this. This approach might not be the most performant way out there, but it works just fine and since modern CPUs all have hardware support for encryption your users will only see a difference in response times if you want to encrypt several big amount of data at once. But should avoid doing that without file encryption anyway.
Let's assume that your application has a FileController which can store and download images. So we start building our secure file storage with your controller's store method.
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
class FileController extends Controller
{
public function store(Request $request): void
{
$request->validate(['picture' => ['required', 'file', 'image']]);
Storage::put('sensitive_user_image.jpg', Crypt::encrypt($request->file('picture')->getContent()));
}
}
This solution seems too simple or? It is only one single line of code. But it does everything what we need. You only have to extract the content of your file put it into Laravel Crypt Facade and there you go. Under the hood the encrypt method will encrypt your files content with OpenSSL and the AES-256-CBC cipher and sign it with a message authentication code.
Then we are storing the encrypted content to the sensitive_user_image.jpg file on our Storage. When we look into this file it shows something like this.
tail storage/app/sensitive_user_image.jpg
dTbUV3RGtNb1FkaXFQVGs4S1pMRzVNRmpoNHpFVDhFTEdnSThmb0wvN0F0U0I4Ym5tUFpaWUE3TnhSWnBuRzBuTHh2Yk5Bd1U1aGdmWW04OHJBVG
I1aHlJMzBsTzdwWDlhWGVlbnFuTWQzUjNoSVpuYWYwWHpBS3dzMkhZMllyMG9MWWtRYmdLOGFpaC9rWW5VOEZUVSsvOGViYzJtc01SMjgwZEFlcV
RxRkYxeituRWlKaWhubzU2TXd6T3BGMkFPTE0wTEdDZWNUcDdDdEc1SlkvenZjL043aXdSREZwRzRmNzd3NGRmaDdBa05nRnJMSVFvNHRjZndmMU
pseHRXLytkdnhKbjdUeGZqZVRMNjRjUEQrcUJNcU5iOTNhME9XTERZVlpuRzgrMGFMME82VVVQakcxNzl6UHJYMXNTUms2Z1RZTkVzWk1LWFZuWk
9KcGdYQkY2aFRMTmE0emZqd0F5TGc9PSIsIm1hYyI6IjU2MDVmZWRlNDZlOWIxMmQ4ZWUxMWVhNzM4MjMwYzM5N2I5YjhjNGJkMTQ1ZDA4OGQwMm
Q2NmYxZDgwYmNiMDkiLCJ0YWciOiIifQ==
Now that we can store and encrypt files lets extend our FileController with a download method so your users can download their previously uploaded files.
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
class FileController extends Controller
{
public function store(Request $request): void
{
$request->validate(['picture' => ['required', 'file', 'image']]);
Storage::put('sensitive_user_image.jpg', Crypt::encrypt($request->file('picture')->getContent()));
}
public function download(): StreamedResponse
{
return response()->streamDownload(function () {
echo Crypt::decrypt(Storage::get('sensitive_user_image.jpg'));
}, 'sensitive_user_image.jpg');
}
}
This time we need a little more code. If we would not have file encryption we would have done this download functionality with the Storage facade
return \Illuminate\Support\Facades\Storage::download('sensitive_user_image.jpg');
But we are doing quite similar things. In our case we have to decrypt our file content to its origin values. So the user gets a real image and not some random char sequences. We do that with the streamDownload() method of the response() helper it accepts a closure as its parameter and everything which gets echoed in this closure will be transferred as a stream to the user. And again we can use the decrypt method on the Crypt facade for that.
Looking at the file encryption feature we are finished, yet we can encrypt and decrypt files without compromising any user experience. Great!
But I was talking something about digital signatures and that our files cannot be manipulated by 3rd parties. So let's try if it works.
So im going to add some characters to our sensitive_user_image.jpg, and we check if they are served to our users.
echo "i am evil" >> storage/app/sensitive_user_image.jpg
Now if we hit our download route we get a sensitive_user_image.jpg download response but the file is empty and cannot be opened. This happens because we are using the streamDownload() method and this method only cares about echoed chars within its closure.
But why is this file empty? Laravel's Crypt facade detects file manipulations through the digital signature as described above. If it detects manipulation a DecryptException is thrown. Since the streamedDownload closure only cares about echos it did not echo anything because we have run into an Exception.
Final words
We have learned how we can improve our applications privacy and make the world a little better and safer. I hope you have enjoyed it!
I always love to have some feedback you can contact me on Twitter or per mail if you want! Have a nice week!
Do you need help with your next project?
Unleash the full potential of your business, and schedule a no-obligation consultation with our team of experts now!