Encryption and the in-between

Apr, 4 2024#tutorial

Last year, we introduced a simple but surprisingly useful feature to Laravel Forge: the ability to add notes to servers.

While checking the uptake of this feature, we noticed that customers were often storing sensitive data in the field. We hadn’t designed notes to store sensitive information, so we found ourselves in a situation where we now needed to encrypt existing unencrypted data, while also allowing for new data to be inserted as encrypted data - at the same time, the dashboard needed to be able to show the notes correctly whether they had been encrypted or not.

Our migration process looked like this:

  1. Run a command that encrypts all existing unencrypted server notes.
  2. Update our model to cast the notes field, encrypting or decrypting as required.

To do this, we leaned on Laravel’s custom casts feature to handle this “sometimes encrypted” data. We created a new cast SometimesEncrypted that allowed us to gracefully decrypt the encrypted notes, or simply return the plaintext version which may have been available during the migration:

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;

class SometimesEncrypted implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  array<string, mixed>  $attributes
     */
    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        if ($value === null || $value === '') {
            return $value;
        }

        try {
            return Crypt::decryptString($value);
        } catch (DecryptException $e) {
            return $value;
        }
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  array<string, mixed>  $attributes
     */
    public function set(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return ($value === null || $value === '') ? null : Crypt::encryptString($value);
    }
}

Using this cast meant that we could also ship this change in two parts:

  1. Cast the notes field, encrypting newly saved server notes as we go.
  2. Migrate the plaintext server notes, by storing them back into the model, and allowing the SometimesEncrypted cast to do its job.

Laravel's custom casts allowed us to implement a seamless transition for our users, encrypting sensitive data without disrupting the user experience or existing functionality. What interesting ways have you found to use Laravel's custom casts?

By James Brooks

Software Developer at Laravel, working on Forge and Envoyer.

Find me on Twitter, GitHub or my blog.

Follow the RSS Feed.