Simon Kollross

Simon Kollross

Software developer, blogger, elegant code enthusiast.

Encrypting Laravel Eloquent attributes using a custom cast

Recently I came across the issue that I wanted to encrypt some Eloquent attributes at-rest before storing them in a database. Encrypting attributes with Laravel's encryption features is especially useful if your app uses a separate database server. If you encrypt attribute values with the app server's APP_KEY, only the app server itself can read those values. In case anyone gets access to your database, only the ciphertext is exposed which is obviously useless without having access to the APP_KEY for decryption. Laravel uses strong state-of-the-art AES-256 encryption by default. Your data really is safe.

To make use of Laravel's built-in encryption features, make sure you have the APP_KEY variable set in your .env file. Since Laravel 7, Eloquent has support for custom casts. Using custom casts we can easily encapsulate transparent attribute encryption and decryption in an EncryptCast class.

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class EncryptCast implements CastsAttributes
{
    public function get($model, string $key, $value, array $attributes)
    {
        return ! is_null($value) ? decrypt($value) : null;
    }

    public function set($model, string $key, $value, array $attributes)
    {
        return [$key => ! is_null($value) ? encrypt($value) : null];
    }
}

Let me show you how to use this cast. Suppose there is a User model and we want to encrypt the user's age.

<?php

namespace App;

use App\Casts\EncryptCast;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $casts = [
        'age' => EncryptCast::class,
    ];

    // ...
}

When we set the user's age somewhere in our application code, the attribute is encrypted. If we access $user->age, the attribute is decrypted. Everything happens automatically behind the scenes.

But there are some things you have to be aware of.

Don't expose the app key. Everyone with access to the APP_KEY can read the encrypted attributes.

Don't loose the app key. Make sure you backup the APP_KEY in a secure location. You cannot restore the data without the key because that's the reason why you're encrypting data.

Don't change the app key. Although regular key rotation might be a good idea to prevent e.g. former employees from gaining access to data, make sure to implement a migration which decrypts the data using the old key and encrypts it again using the new key before changing the APP_KEY.

Make sure database columns have a proper type. When encrypting existing values in your database, create a migration that changes the column type to something like text. The final length of the ciphertext is hardly predictable and probably longer than the plaintext. If the database silently cuts off some characters from the ciphertext, it cannot be decrypted and the data is lost.

Use a migration to encrypt existing values. Encrypt existing attribute values that will use the EncryptCast in a migration, so the values can be read after the code has been deployed. Take care, Laravels encrypt function also encrypts NULL values. If NULL values should stay NULL, you have to handle that case in your migration. This also reflects the behavior of the EncryptCast shown above.

I hope you've learnt something from this post. If you're interested in IT security and data protection, follow @skollro on Twitter or subscribe to my newsletter. I also hope to resume the work on my eBook GDPR for Developers soon, where I try to learn you some techniques on how to make Laravel applications more secure.