stueynet's avatar

Encrypting model data

I am working on a new project that stores medical information. For security I thought it would be cool to store all the personal data in the database with encryption. Is there any specific workflow for doing this. Essentially setting encryption on certain Model fields and having everything just work. Thinking about it, I feel like its not so simple but I thought I would throw it out there.

0 likes
72 replies
martinbean's avatar
Level 80

@stueynet You would probably have the encryption at the database level, as if someone gets access to the database you don’t want them to be able to read people’s medical data in plain text.

You could create a trait that encrypts and decrypts data on save and retrieval respectively:

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

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

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

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

You can then just apply the trait to your models, and define a property called $encryptable that’s an array of columns whose data should be encrypted:

class Patient extends Model
{
    use Encryptable;

    protected $encryptable = [
        'blood_type',
        'medical_conditions',
        'allergies',
        'emergency_contact_id',
    ];
}
69 likes
yvomenezes's avatar

@martinbean, thanks for this! It helped me a lot. However, there is a small error in your code. The getAttribute function should return $value and the setAttribute function should return nothing. I'll put here my custom version of your code.

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

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

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

        parent::setAttribute($key, $value);
    }
}
martinbean's avatar

@yvomenezes My example is over six years old now. Laravel has since added native encryption casting to Eloquent models.

I handled returns from my overloaded setAttribute method because the parent setAttribute method in Eloquent models potentially returns: https://github.com/laravel/framework/blob/9.x/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php#L894-L901

If I had just returned nothing, then it could have potentially broken something.

3 likes
stueynet's avatar

This looks amazing. Implementing now and will let you know how it goes.

1 like
stueynet's avatar

Out of the box this is working perfectly so far. The only change you need is you forgot to return $value; in the getAttribute method. Aside form that this works without a hitch. Will do more testing and report back. F'n thanks so much!

1 like
stueynet's avatar

My god this is sick. I cannot believe it took me 3 minutes to implement this. Just one more questions @martinbean. Can you tell me how this is being encrypted? So for example if for some reason I had to transfer the database or restore an old backup etc... What key is used to encrypt / decrypt the info. And obviously if that key every changed we would be in deep trouble correct?

1 like
martinbean's avatar

@stueynet Glad to have been of help! :)

The data will be encrypted using the algorithm specified in your config/app.php file, and the key will be the APP_KEY value in your .env file.

1 like
stueynet's avatar

Ok I am getting this now. So just did some testing and tried changing my key and all was bad. So if you DO encrypt the database data, you must retain the key to decrypt it. And if you decide to go with Encryption on the database, you must NEVER change the key right? Otherwise data created with the old key will not be able to be decoded. Thanks again for all the help.

martinbean's avatar

@stueynet Yes, you’ll have to retain the key. That’s have encryption works. If you lose your key then you’re going to have difficulty getting that data back again.

ShaunL's avatar

@martinbean this was a great thread, I just have one question regarding the encryption he is trying to achieve. If he encrypt the same value (example first name Dan), with the same key, with the same algo. Wouldn't the output be the same to the database? Is it possible to store a salt and mix up the encrypted value even more and decrypt it with the salt so you wouldn't have the situation where you have 7 entries into your table that happen to look exactly the same?

The reason I ask is if you had someone look at the data, they possibly could deduce what the information was that is encrypted. Maybe I'm thinking too much into it?

stueynet's avatar

@martinbean yeah this is pretty scary stuff. Something to think long and hard about. Thanks again.

HRcc's avatar

@ShaunL No, it won't be the same, since there is a presence of randomized initialization vector in the Encrypter class. You can easily test that in tinker by calling Crypt::encrypt('secret stuff') twice or more times.

// edited

3 likes
RobinMalfait's avatar

@HRcc bcrypt() is not encrypting, that is hashing, which is not reversable, so don't use bcrypt() if you need to re-engineer to original data!

2 likes
HRcc's avatar

Eh, my bad, switched it for the correct example. Thanks for pointing that out.

martinbean's avatar

@ShaunL @HRcc beat me to it (I have to sleep some time ;) ), encrypting the same string more than once will yield different strings, but they’ll be able to be decrypted back to their original form with no problem.

1 like
entreredes's avatar

Hi,

I have a problem with this.

I'm trying to do a query to a model.

$userDataId="123456"; $socialUser= Socialuser::where('id','=',$userDataId)->first();

the id is encrypted in the model with the trait encryptable but I get "null". In SocialUser table I insert an user with userDataId , and I see the data encrypted in database. Why can't i get the data?

thx

llevvi's avatar

I implemented this solution in my application and it worked fine in a first moment. However, now I am unable to login any user, it keeps saying: "These credentials do not match our records."

I am only encrypting the fields "email" and "company". I deactivated the encryption for "email", created a new user and I still can't get it logged in.

That's how my full classes look like:

User.php

<?php

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Encryptable;
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'first_name', 'middle_name','last_name','company', 'phone', 'email', 'password',
    ];

    protected $encryptable = [
        'company','email',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
}

Encryptable.php

<?php

namespace App;

use Crypt;

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

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

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

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

What I am doing wrong?

Thanks in advance.

1 like
llevvi's avatar

I made more tests. Apparently my issue is in the email field being encrypted. I can login with a user that doesn't have their email encrypted. I believe the email is not being decrypted properly :/

mstrauss's avatar

The selected answer was an excellent solution. Quick question though... what about type/length for the encrypted fields? Is there a standard, multiply acceptable input size by four or five convention, in order to determine the appropriate length? Also is it better to go with VarChar or LongText for the encrypted field type?

bashy's avatar

@mstrauss Depends on the content but text() would be acceptable for most inputs. Read some posts about it via Google.

2 likes
DutchGrover's avatar

Excellent solution, it's a very flexible solution when you have a lot of attributes you want encrypted on multiple models. It fits the question 100%. Kudos to @martinbean for posting it.

Question: image another scenario.

You only want encrypt 1 or 2 attributes on a specific model, maybe it's best to use the default accessors & mutators structure? See: https://laravel.com/docs/5.2/eloquent-mutators

Or is the best solution also best in that scenario?

Michael_Johnson's avatar

Great answer exactly what was needed!!!!!

Does it matter where you store the trait? Would it be acceptable to create a Traits folder and keep all traits together in there??

Thanks again!

gibex's avatar

@ llevvi, yes, because Auth::attempt is using clean email against encrypted.

Does anybody know a quick solution vs. UserProvider class?

Thanks @martinbean for solution

shanely's avatar

@martinbean ,

can you please explain why you only declare getAttribute or setAttribute ?,why it's not getMedicineAttribute ? or setMedicineAttribute ?

     public function getAttribute($key){}
    
      
     public function setAttribute($key, $value){}
    ```


Thank you
llevvi's avatar

@gibex Isn't any way to change Auth:: behavior by making it instantiate an user object instead of pulling the email directly from the database?

gibex's avatar

@llevvi , for Auth you can use a Custom User provider class. Check here and here

The Illuminate\Auth\EloquentUserProvider is another place to check how thing works behind the scenes.

Anyway, search (Model::where) is slow,an alternative hash column for email is required to avoid bottlenecks. You can work with this and this

1 like
gibex's avatar

@shanely , I think it depends on use cases (here set(get)Attribute are using existing attributes)

setAttribute - set existing attribute (internally the method is using setNameAttribute if defined)

setNameAttribute - is to format existing attribute OR create a new one

shanely's avatar

@gibex,

if we say setAttribute($key,$value) ? how does this will work ?

Thank you.

gibex's avatar

The code is self explanatory. The method is part of Model class.

    public function setAttribute($key, $value)
    {
        // First we will check for the presence of a mutator for the set operation
        // which simply lets the developers tweak the attribute as it is set on
        // the model, such as "json_encoding" an listing of data for storage.
        if ($this->hasSetMutator($key)) {
            $method = 'set'.Str::studly($key).'Attribute';

            return $this->{$method}($value);
        }

        // If an attribute is listed as a "date", we'll convert it from a DateTime
        // instance into a form proper for storage on the database tables using
        // the connection grammar's date format. We will auto set the values.
        elseif ($value && (in_array($key, $this->getDates()) || $this->isDateCastable($key))) {
            $value = $this->fromDateTime($value);
        }

        if ($this->isJsonCastable($key) && ! is_null($value)) {
            $value = $this->asJson($value);
        }

        $this->attributes[$key] = $value;

        return $this;
    }

Next

Please or to participate in this conversation.