ACR's avatar

namespace App\Model;

use Illuminate\Database\Eloquent\Model; use App\Support\UuidScopeTrait; use App\Support\EncryptableTrait;

class Task extends Model { use UuidScopeTrait,EncryptableTrait;

// protected $guarded = ['uuid'];

protected $table='tasks';

    protected $fillable=[
        
        'title',
        'uuid',
    
    ];

protected $encryptable = [
    'title'
];

}

milescato's avatar

Very helpful discussion!!! Thanks to @martinbean

I found in 5.2, when I implemented the new getAttribute() function as described above, when starting with a new model instance, getAttribute() struggled with an empty $value for a field in the $encryptable array. More specifically the the getJsonPayload() in BaseEncrypter.php threw a 'The payload is invalid' error.

Adding a test for an empty $value worked nicely, as seen below.

public function getAttribute($key)
{
    $value = parent::getAttribute($key);

    if (in_array($key, $this->encryptable)) {
        $value = empty($value) ? null : Crypt::decrypt($value);
    }

    return $value;
}

public function setAttribute($key, $value)
{
    if (in_array($key, $this->encryptable)) {
        $value = empty($value) ? null : Crypt::encrypt($value);
    }

    return parent::setAttribute($key, $value);
}

In my application, these fields are signatures. An empty field with, 0 or '' value is encrypted, so setting the otherwise empty field to null is necessary to determine if the signature is missing.

2 likes
eugenefvdm's avatar

Hi @milescato and @martinbean ,

Thank you for this incredible post. I concur with @milescato that to avoid "The payload is invalid" you have to check for empty values. I found this out by just copying / pasting @martinbean 's original example in my Laravel Nova application. The other question I have is why Martin's original example didn't include the return statement? Anyway, for it's working beautifully and I love the elegant way one is able to do these things in Laravel. What a winner!

click's avatar

Just an update for anyone passing by this topic via Google. In Laravel 7 you can create your own Casts

So if you only want to encrypt a few fields from a model you can easily do this with this feature.

app/Casts/Encrypted.php

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class Encrypted implements CastsAttributes
{
    public function get($model, $key, $value, $attributes)
    {
        // note that this could throw an exception when $value 
        // can't be decrypted.
        return $value === null ? null : \Crypt::decrypt($value);
    }

    public function set($model, $key, $value, $attributes)
    {
        return $value === null ? null : \Crypt::encrypt($value);
    }
}

app/Models/YourModel.php

class YourModel extends Model
{
    protected $casts = [
        'love_note' => \App\Casts\Encrypted::class,
    ];
}
$model->love_note = 'Something that needs that extra protection'; 
$model->save(); 

More info regarding custom casts https://laravel.com/docs/7.x/eloquent-mutators#custom-casts

Luis Mata's avatar

Martin,would you do this same approach nowdays?

jivanrij's avatar

I used the example of Martin, and it worked in tinker, but when I started working with the models in Nova I got the error that the payload was invalid. This was due to the Crypt::decrypt() getting a null value when I used the create form. So I added some checks in getAttribute. And it works now.

Encrypting fields of a model

Add the following Trait to your application. This Trait encrypts and decrypts all the fields in the encryptable array you need to define in the model.

namespace App\Traits;

use Illuminate\Support\Facades\Crypt;

trait Encryptable
{
    public function getAttribute($key)
    {
        if (in_array($key, $this->encryptable)) {
            if (parent::getAttribute($key)) {
                return Crypt::decrypt(parent::getAttribute($key));
            }
        }

        return parent::getAttribute($key);
    }

    public function setAttribute($key, $value)
    {
        if (in_array($key, $this->encryptable)) {
            $value = Crypt::encrypt($value);
        }

        return parent::setAttribute($key, $value);
    }
}

Add the following to your model


use App\Traits\Encryptable;

class PersonalData extends Model
{
    use Encryptable;

    protected $encryptable = [
        'field_one',
        'field_two'
    ];
}
mimotic's avatar

I added two things to complete this feature:

1.- Old Data

If you have any data saved before this change, yo will have an exception when you try to decrypt no encrypted data. I added an exception handler block.

2.- If you directly call the data you will get it encrypted, then I added the attributesToArray method to this class

namespace App\Http\Traits;

use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;

trait EncryptPersonalData
{

    public function getAttribute($key)
    {
        if (in_array($key, $this->encryptable)) {
            if (parent::getAttribute($key)) {
                try {
                    $decrypted = Crypt::decryptString(parent::getAttribute($key));
                } catch (DecryptException $exception) {
                    $decrypted = parent::getAttribute($key);
                }
                return $decrypted;
            }
        }

        return parent::getAttribute($key);
    }

    public function setAttribute($key, $value)
    {
        if (in_array($key, $this->encryptable)) {
            $value = Crypt::encryptString($value);
        }

        return parent::setAttribute($key, $value);
    }

    public function attributesToArray()
    {
        $attributes = parent::attributesToArray();

        foreach ($attributes as $key => $value)
        {
            if (in_array($key, $this->encryptable))
            {
                try {
                    $decrypted = Crypt::decryptString($value);
                } catch (DecryptException $exception) {
                    $decrypted = $value;
                }
                $attributes[$key] = $decrypted;
            }
        }

        return $attributes;
    }
}
1 like
jamesautodude's avatar

To anyone coming across this later like I did...

In newest versions, you can just use casts in the model, and put it as "encrypted" like:

protected $casts = [
    'email' => 'encrypted',
    'phone' => 'encrypted',
    'salary' => 'encrypted'
];

And Laravel will handle the encrypting and decrypting all on its own while you just make calls/updates/inserts as normal

Previous

Please or to participate in this conversation.