A Guide to encryption and hashing in Laravel

honeybadger_staff

Honeybadger Staff

Posted on March 11, 2024

A Guide to encryption and hashing in Laravel

This article was originally written by Ashley Allen on the Honeybadger Developer Blog.

Every web developer should be aware of hashing and encryption because they are vital security concepts. When used correctly, they can improve web application security and help ensure the privacy of users' personally identifiable information (PII).

In this article, we'll explore hashing and encryption, including their differences. We'll also learn how to use them within Laravel projects to improve application security. We'll cover how to hash data and compare it to plain-text values. We'll also explain the different ways that you can encrypt and decrypt data in Laravel.

What are hashing and encryption?

As a web developer, you've likely encountered the terms "encryption" and "hashing". You may have even used them in your own projects, but what do they mean, and how are they different?

These terms are sometimes used interchangeably, but they are different techniques with their own use cases.

Encryption and hashing are both forms of "cryptographic security" and consist of using mathematical algorithms to transform data into a form that is unreadable to humans and (mostly) secure.

The world of cryptography is complex and relies heavily on mathematics. Therefore, for the sake of this article, we'll keep things simple and focus on the high-level concepts of both techniques and how they can be used in your Laravel applications.

Encryption

Let's start by taking a look at encryption.

Encryption is the process of encoding information (usually referred to as "plaintext") into an alternative form (usually referred to as "ciphertext"). Typically, ciphertext is generated using a "key" and an encryption algorithm. It can then only be decrypted and read by someone who has the correct key, depending on the type of encryption used.

Encryption can come in two forms: "symmetric" and "asymmetric".

Symmetric encryption uses the same key for both encryption and decryption. This means that the same key is used to encrypt and decrypt the data. Examples of this type of encryption include Advanced Encryption Standard (AES) and Data Encryption Standard (DES). Laravel uses AES-256-CBC encryption by default.

Asymmetric encryption is a bit more complicated and uses a pair of keys: a public key and a private key. The public key is used to encrypt the data, and the private key is used to decrypt the data. Thus, the public key can be shared with anyone, but the private key must be kept secret, as anyone with it can successfully decrypt the data. Examples of this type of encryption include Rivest–Shamir–Adleman (RSA) and Diffie-Hellman.

As an example of what an encrypted string might look like, let's say that we have a string of text that we want to encrypt. We'll use the following string as an example:

This is a secret message.
Enter fullscreen mode Exit fullscreen mode

If we encrypted this text using AES-256-CBC encryption and used U2x2QdvosFTtk5nL0ejrKqLFP1tUDtSt as the key, we would get the following ciphertext:

eyJpdiI6IjF1cmF0YU5TMkRnR3NUMVRMMm1udFE9PSIsInZhbHVlIjoieGloYW5VVWtXV2hjcVRVY3hGTkZ1bDdoOVBSZEo1VkVWNE1LSlB5S0lhNkF3SloxeWhRejNwbjN5SEgxeUJXayIsIm1hYyI6IjljNzY0MTBmMGJlZmRjNzcwMjFiMmFjYmJhNTNkNWVhODkxMTgzYmYwMjA3N2YzMjM1YmVhZWU4NDRiOTYzZWQiLCJ0YWciOiIifQ==
Enter fullscreen mode Exit fullscreen mode

If we were then to decrypt the above ciphertext using the same key, we would get the original plaintext:

This is a secret message.
Enter fullscreen mode Exit fullscreen mode

Hashing

Hashing is a bit different from encryption. It is a one-way process, meaning that when data is "hashed", it's transformed into what is referred to as a "hash value". However, unlike encryption, you cannot reverse the process and get the original data. Hence, the hash value is not reversible.

Typically, you’d want to use a hash in a situation where you don't want any data to be retrieved in the event of a leak. For example, you may want to store a user's password in your database but wouldn't want to store it in plain text in case the database was ever compromised. If you did store it in plain text, you would potentially be giving malicious hackers a list of passwords in use. In an ideal world, all users would use unique passwords, but we all know that this isn't the case. Therefore, a leaked database of plain-text passwords would be a goldmine for hackers because they could then try to use the passwords on other websites or applications. To combat this problem, we store passwords in a hashed format.

For example, let's say that a user's password is "hello". If we were to use the bcrypt algorithm to hash this password, it would look something like this:

$2y$10$XY2DMYKvLrMj7yqYwyvK5OSvKTA6HOoPTpe3gVpVP5.Y4kN1nbOLq
Enter fullscreen mode Exit fullscreen mode

An important part of hashing is the use of a "salt". A salt is a random string of characters used to make the hashed values of the same data different. This means that if two users have the same password, their hashed passwords would be stored differently in the database. This is important because it would stop hackers from being able to deduce which users had the same password. For instance, the following strings are all hashed versions of hello:

$2y$10$NUgYbLrzxn471GzcIN10wedXEcltcbAasHqU7hCeMFv4aCTl/6bVW
Enter fullscreen mode Exit fullscreen mode
$2y$10$AvBxO6HCRwYPNPZmeERIEOzLAJP7ZkcjrekdzaRLwY8YX4m9VJiFy
Enter fullscreen mode Exit fullscreen mode
$2y$10$YQ3lzNx8h0tDgw4K3dzJAOxycZhDhTAnueSugbmoo3NDTuq1OT8KW
Enter fullscreen mode Exit fullscreen mode

You're probably thinking, "If it's hashed and can't be reversed, how do we know if the password is correct?". We can do this by comparing the hash value of the password that the user has entered with the hash value of the password that is stored in the database by using the password_verify() function PHP provides. If they match, then we know that the password is correct. If they don't match, then we know that the password is incorrect. Later in this article, we'll cover how you can do this comparison in Laravel.

Many hashing algorithms are available, such as bcrypt, argon2i, argon2id, md5, sha1, sha256, sha512, and many more. However, when dealing with passwords, you should always use a hashing algorithm designed to be slow, such as bcrypt, because it makes it more difficult for hackers to brute-force the passwords.

Hashing in Laravel

Now that we have a basic idea of what hashing is, let's take a look at how hashing works within the Laravel framework.

Hashing passwords

As mentioned previously, you don't want to store users' passwords as plain text in a database. Instead, you'll want to store their passwords in a hashed format. This is where the Hash facade comes into play.

By default, Laravel supports three different hashing algorithms: "Bcrypt", "Argon2i", and "Argon2id". By default, Laravel uses the "Bcrypt" algorithm, but if you want to use a different one, Laravel allows you to change it. We'll cover this topic in more detail later in the article.

To get started with hashing a value, you can use the Hash facade. For example, let's say that we want to hash the password "hello":

use Illuminate\Support\Facades\Hash;

$hashedValue = Hash::make('hello');

// $hashedValue = $2y$10$XY2DMYKvLrMj7yqYwyvK5OSvKTA6HOoPTpe3gVpVP5.Y4kN1nbOLq
Enter fullscreen mode Exit fullscreen mode

As you can see, it's really easy to hash a value.

To give this a bit of context, let's look at how it might work in a controller in your application. Imagine that you have a PasswordController that allows authenticated users to update their password. The controller may look something like so:

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

final class PasswordController extends Controller
{
    public function update(Request $request)
    {
        // Validate the new password here...

        $request->user()->fill([
            'password' => Hash::make($request->newPassword)
        ])->save();
    }
}
Enter fullscreen mode Exit fullscreen mode

Comparing hashed values to plain text

As previously mentioned, it's not possible to reverse a hashed value. Therefore, if we want to determine the hashed value, we can only do so by verifying the hashed value against a plain-text value. This is where the Hash::check() method comes into play.

Let's imagine that we want to determine whether the "hello" plain-text password is the same as the hashed password that we created earlier. We can do this by using the Hash::check() method:

$plainTextPassword = 'hello';
$hashedPassword = "$2y$10$XY2DMYKvLrMj7yqYwyvK5OSvKTA6HOoPTpe3gVpVP5.Y4kN1nbOLq";

if (Hash::check($plainTextPassword, $hashedPassword)) {
    // The passwords match...
} else {
    // The passwords do not match...
}
Enter fullscreen mode Exit fullscreen mode

The Hash::check() method will return true if the plain-text password matches the hashed password. Otherwise, it will return false.

This type of approach is what would typically be used when a user is logging into your application. If we were to ignore any additional security measures (such as rate limiting) for our login form, your login flow might be following the steps below:

  • The user provides an email and password.
  • Attempt to retrieve a user from the database with the given email.
  • If a user is found, then compare the given password with the hashed password stored in the database by using the Hash::check method.
  • If the Hash::check method returns true, then the user has successfully logged in. Otherwise, they've entered the incorrect password.

Hash drivers in Laravel

Laravel provides the functionality for you to choose between different hashing algorithms. By default, Laravel uses the "Bcrypt" algorithm, but you can change it to either the "Argon2i" or the "Argon2id" algorithm, which are also supported. Alternatively, you can implement your own hashing algorithm, but this is strongly discouraged because you may introduce security vulnerabilities into your application. Instead, you should use one of the algorithms provided through PHP so that you can be sure the algorithms are tried and tested.

To change the hashing algorithm being used across your entire application, you can change the driver value in the config/hashing.php config file. The default value is bcrypt, but you can change this to either argon or argon2id, like so:

return [

    // ...

    'driver' => 'bcrypt',

    // ...

];
Enter fullscreen mode Exit fullscreen mode

Alternatively, if you'd prefer to explicitly define the algorithm that should be used, you can use the driver method on Hash facade to determine which hashing driver is used. For example, if you wanted to use the "Argon2i" algorithm, you could do the following:

$hashedValue = Hash::driver('argon')->make('hello');
Enter fullscreen mode Exit fullscreen mode

Encryption in Laravel

Let's now take a look at how to use encryption in Laravel.

Encrypting and decrypting values

To get started with encrypting values in Laravel, you can make use of the encrypt() and decrypt() helper functions or the Crypt facade, which provides the same functionality. For example, let's say that we want to encrypt the string "hello":

$encryptedValue = encrypt('hello'); 
Enter fullscreen mode Exit fullscreen mode

By running the above, the encryptedValue variable would now be equal to something like this:

eyJpdiI6IitBcjVRanJTN3hTdnV6REdScVZFMFE9PSIsInZhbHVlIjoiZGcycC9pTmNKRjU3RWpmeW1GdFErdz09IiwibWFjIjoiODg2N2U0ZTQ1NDM3YjhhNTFjMjFmNmE4OTA2NDI0NzRhZmI2YTg5NzEwYjdmY2VlMjFhMGZhYzE5MGI2NDA3NCIsInRhZyI6IiJ9
Enter fullscreen mode Exit fullscreen mode

Now that we have the encrypted value, we can decrypt it by using the decrypt() helper function:

$encryptedValue = 'eyJpdiI6IitBcjVRanJTN3hTdnV6REdScVZFMFE9PSIsInZhbHVlIjoiZGcycC9pTmNKRjU3RWpmeW1GdFErdz09IiwibWFjIjoiODg2N2U0ZTQ1NDM3YjhhNTFjMjFmNmE4OTA2NDI0NzRhZmI2YTg5NzEwYjdmY2VlMjFhMGZhYzE5MGI2NDA3NCIsInRhZyI6IiJ9';

$decryptedValue = decrypt($encryptedValue);
Enter fullscreen mode Exit fullscreen mode

By running the above, the decryptedValue variable would now be equal to "hello".

If any incorrect data were passed to the decrypt() helper function, an Illuminate\Contracts\Encryption\DecryptException exception would be thrown.

As you can see, encrypting and decrypting data in Laravel is relatively easy. It can simplify the process of storing sensitive data in your database and then decrypting it when you need to use it.

Changing the encryption key and algorithm

As mentioned earlier in the article, Laravel uses the "AES-256" symmetric encryption algorithm by default, meaning that a single key is used for encrypting and decrypting data. By default, this key is the APP_KEY value defined in your .env file. This is very important to remember because if you change your application's APP_KEY, then any encrypted data that you have stored can no longer be decrypted (without making changes to your code to explicitly use the older key).

If you wish to change the encryption key without changing your APP_KEY, you can do so by changing the key value in the config/app.php config file. Likewise, you can also change the cipher value to specify the encryption algorithm used. The default value is AES-256-CBC, but can be changed to AES-128-CBC, AES-128-GCM, or AES-256-GCM.

Automatically encrypting model attributes

If your application is storing sensitive information in the database, such as API keys or PII, you can use make use of Laravel's encrypted model cast. This model cast automatically encrypts data before storing it in the database and then decrypts it when it's retrieved.

This is a useful feature because it allows you to continue working with your data as if it weren't encrypted, but it's stored in an encrypted format.

For example, to encrypt the my_secret_field on a User model, you could update your model like so:

class User extends Model
{
    protected $casts = [
        'my_secret_field' => 'encrypted',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Now that it's defined as an accessor, you can continue using the field as usual. Thus, if we wanted to update the value stored in the my_secret_field, we could still use the update method like so:

$user->update(['my_secret_field' => 'hello123']);
Enter fullscreen mode Exit fullscreen mode

Notice how we didn't need to encrypt the data before passing it to the update method.

If you were to now inspect the row in the database, you wouldn't see "hello123" in the user's my_secret_field field. Instead, you'd see an encrypted version of it, such as the following:

eyJpdiI6IjM3MUxuV0lKc2RjSGNYT2dXanhKeXc9PSIsInZhbHVlIjoiNmxPZjUray9ZV21Ba1RnRkFNdHRTZz09IiwibWFjIjoiNTNlNmU0YTY5OGFjZWU2OGJiYzY4OWYzYzExYjMzNTI0MDQ2YTJiM2M4YWZkMjkyMGQxNmQ2MmYwNzQyNGFjYSIsInRhZyI6IiJ9
Enter fullscreen mode Exit fullscreen mode

Thanks to the encrypted model cast, we would still be able to use the intended value of the field. For example,

$result = $user->my_secret_field;

// $result is equal to: "hello123"
Enter fullscreen mode Exit fullscreen mode

As you can imagine, using the encrypted model cast is a great way to quickly add encryption to an application without having to manually encrypt and decrypt the data. Hence, it is a quick way to improve data security.

However, it does have some limitations, of which you should be aware. First, because the encryption and decryption are run when storing and fetching the Model, calls using the DB facade won't automatically do the same. Therefore, if you intend to make any queries to the database using the DB facade (rather than using something like Model::find() or Model::get()), you'll need to manually handle the encryption.

Furthermore, it's worth noting that although encrypting fields in the database improves security, it doesn't mean the data are completely secure. If a malicious attacker finds the encryption key, they could decrypt the data and access the sensitive information. Therefore, encrypting the fields is beneficial only if the database is compromised. If you're using a key (such as your application's APP_KEY) that's stored in your .env file, then a breach of your application server would also allow the attacker to decrypt the data.

Using a custom encryption key when manually encrypting data

When encrypting and decrypting data, there may be times when you want to use a custom encryption key. You might want to do this to avoid coupling your encrypted data to your application's APP_KEY value. Alternatively, you may want to give users the ability to define their own encryption keys so that (theoretically) only they can decrypt their own data.

If you're manually encrypting and decrypting data, to define your own encryption key, you can instantiate the \Illuminate\Encryption\Encrypter class and pass the key to the constructor. For example, let's imagine that we want to encrypt some data using a different encryption key. Our code may look something like so:

use Illuminate\Encryption\Encrypter;

// Our custom encryption key:
$key = 'U2x2QdvosFTtk5nL0ejrKqLFP1tUDtSt';

$encrypter = new Encrypter(
    key: $key,
    cipher: config('app.cipher'),
);

$encryptedValue = $encrypter->encrypt('hello');
Enter fullscreen mode Exit fullscreen mode

As you can see, this is easy to do and adds the flexibility to use a custom encryption key. Now, if we were to try and decrypt the $encryptedValue variable, we would need to use the same key that we used to encrypt it and wouldn't be able to run the decrypt() helper function because it wouldn't be using the correct key.

Using a custom encryption key when using model casts

If you're using the encrypted model cast, as we've covered earlier in this article, and you want to use a custom key for the encrypted fields, you can define your own encrypter for Laravel to use by using the Model::encryptUsing method.

We'd typically want to do this within a service provider (such as the App\Providers\AppServiceProvider) so that the custom encrypter is defined and ready to use when the application starts.

Let's take a look at an example of how we could use the Model::encryptUsing method within our AppServiceProvider:

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Encryption\Encrypter;

final class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->defineCustomModelEncrypter();
    }

    private function defineCustomModelEncrypter(): void
    {
        // Our custom encryption key:
        $key = 'U2x2QdvosFTtk5nL0ejrKqLFP1tUDtSt';

        $encrypter = new Encrypter(
            key: $key,
            cipher: config('app.cipher'),
        );

        Model::encryptUsing($encrypter);
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

As you can see in the example above, defining the custom encrypter is very similar to how we defined it manually earlier in this article. The only difference is that we're passing the Encrypter object to the Model::encryptUsing method so that Laravel can use it behind-the-scenes for us.

Encrypting your .env file

As of Laravel 9.32.0, your application's .env file can also be encrypted. This is useful if you want to store sensitive information in your .env file to your source control (such as git) but don't want to store it in plain text. This is also beneficial because it allows a local versions of your .env config variables to be shared with other developers on your team for local development purposes.

To encrypt your environment file, you'll need to run the following command in your project root:

php artisan env:encrypt
Enter fullscreen mode Exit fullscreen mode

This will generate an output similar to this:

INFO  Environment successfully encrypted.  

Key ........................ base64:amNvB/EvaX1xU+5R9Z37MeKR8gyeIRxh1Ku0pqNlK1Y
Cipher ............................................................ AES-256-CBC
Encrypted file ................................................. .env.encrypted
Enter fullscreen mode Exit fullscreen mode

As we can see from the output, the command has used the key base64:amNvB/EvaX1xU+5R9Z37MeKR8gyeIRxh1Ku0pqNlK1Y to encrypt the .env file using the AES-256-CBC cipher and then stored the encrypted value in the .env.encrypted file.

This means that we can now store that file in our source control, and other developers on our team can use the same key to decrypt the file and use the values within it.

To decrypt the file, you need to run the php artisan env:decrypt command and pass the key used to encrypt the file. For example,

php artisan env:decrypt base64:amNvB/EvaX1xU+5R9Z37MeKR8gyeIRxh1Ku0pqNlK1Y
Enter fullscreen mode Exit fullscreen mode

This will then decrypt the .env.encrypted file and store the decrypted values in the .env file.

Conclusion

Hopefully, reading this article has given you a basic understanding hashing and encryption, as well as the differences between them. It should also have given you the confidence to use both concepts within Laravel projects to improve the security of your applications.

💖 💪 🙅 🚩
honeybadger_staff
Honeybadger Staff

Posted on March 11, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related