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.
If we encrypted this text using AES-256-CBC encryption and used U2x2QdvosFTtk5nL0ejrKqLFP1tUDtSt
as the key, we would get the following ciphertext:
eyJpdiI6IjF1cmF0YU5TMkRnR3NUMVRMMm1udFE9PSIsInZhbHVlIjoieGloYW5VVWtXV2hjcVRVY3hGTkZ1bDdoOVBSZEo1VkVWNE1LSlB5S0lhNkF3SloxeWhRejNwbjN5SEgxeUJXayIsIm1hYyI6IjljNzY0MTBmMGJlZmRjNzcwMjFiMmFjYmJhNTNkNWVhODkxMTgzYmYwMjA3N2YzMjM1YmVhZWU4NDRiOTYzZWQiLCJ0YWciOiIifQ==
If we were then to decrypt the above ciphertext using the same key, we would get the original plaintext:
This is a secret message.
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
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
$2y$10$AvBxO6HCRwYPNPZmeERIEOzLAJP7ZkcjrekdzaRLwY8YX4m9VJiFy
$2y$10$YQ3lzNx8h0tDgw4K3dzJAOxycZhDhTAnueSugbmoo3NDTuq1OT8KW
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
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();
}
}
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...
}
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
andpassword
. - 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 theHash::check
method. - If the
Hash::check
method returnstrue
, 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',
// ...
];
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');
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');
By running the above, the encryptedValue
variable would now be equal to something like this:
eyJpdiI6IitBcjVRanJTN3hTdnV6REdScVZFMFE9PSIsInZhbHVlIjoiZGcycC9pTmNKRjU3RWpmeW1GdFErdz09IiwibWFjIjoiODg2N2U0ZTQ1NDM3YjhhNTFjMjFmNmE4OTA2NDI0NzRhZmI2YTg5NzEwYjdmY2VlMjFhMGZhYzE5MGI2NDA3NCIsInRhZyI6IiJ9
Now that we have the encrypted value, we can decrypt it by using the decrypt()
helper function:
$encryptedValue = 'eyJpdiI6IitBcjVRanJTN3hTdnV6REdScVZFMFE9PSIsInZhbHVlIjoiZGcycC9pTmNKRjU3RWpmeW1GdFErdz09IiwibWFjIjoiODg2N2U0ZTQ1NDM3YjhhNTFjMjFmNmE4OTA2NDI0NzRhZmI2YTg5NzEwYjdmY2VlMjFhMGZhYzE5MGI2NDA3NCIsInRhZyI6IiJ9';
$decryptedValue = decrypt($encryptedValue);
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',
];
}
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']);
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
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"
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');
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);
}
// ...
}
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
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
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
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.