Skip to content

Add Deterministic Encryption Support to Laravel Encrypter #56190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from

Conversation

mostafamaklad
Copy link

@mostafamaklad mostafamaklad commented Jul 1, 2025

Summary

This PR introduces deterministic encryption functionality to Laravel's encryption system, allowing developers to encrypt data in a way that produces consistent, repeatable results for the same input and key combination.

Motivation

Traditional encryption in Laravel is non-deterministic, meaning the same plaintext encrypted multiple times will produce different ciphertext each time due to random initialization vectors (IVs). While this provides excellent security for most use cases, there are specific scenarios where deterministic encryption is valuable:

  • Database indexing: Creating searchable encrypted indexes where you need to find exact matches
  • Caching systems: Generating consistent cache keys from encrypted data
  • Data deduplication: Identifying duplicate encrypted records
  • Testing and debugging: Producing predictable outputs for unit tests

Changes

New Parameters

Added an optional parameter to encryption methods: $deterministic

  • encrypt($value, $serialize = true, $deterministic = false)
  • encryptString($value, $deterministic = false)

New Eloquent Casts

Added deterministic encryption casts for Eloquent models:

    • Basic deterministic encryption encrypted:deterministic
    • Deterministic encryption with array casting encrypted:deterministic,array
    • Deterministic encryption with JSON casting encrypted:deterministic,json
    • Deterministic encryption with object casting encrypted:deterministic,object
    • Deterministic encryption with collection casting encrypted:deterministic,collection

Implementation Details

When $deterministic = true:

  • The IV is generated deterministically using hash('sha256', $data . $key) instead of random bytes
  • Same input + same key = same encrypted output
  • Still cryptographically secure as the IV is derived from both the data and the secret key

When (default): $deterministic = false

  • Maintains existing behavior with random IVs
  • Each encryption produces different output for the same input

Backward Compatibility

This change is fully backward compatible:

  • Default behavior remains unchanged (non-deterministic)
  • All existing code continues to work without modification
  • New parameter is optional with safe defaults

Usage Examples

$encrypter = new Encrypter('your-secret-key');

// Non-deterministic (default) - different output each time
$encrypted1 = $encrypter->encrypt('sensitive data');
$encrypted2 = $encrypter->encrypt('sensitive data');
// $encrypted1 !== $encrypted2

// Deterministic - same output for same input
$encrypted1 = $encrypter->encrypt('sensitive data', true, true);
$encrypted2 = $encrypter->encrypt('sensitive data', true, true);
// $encrypted1 === $encrypted2

// String encryption with deterministic flag
$encrypted = $encrypter->encryptString('user@example.com', true);

// Eloquent model with deterministic casts
class User extends Model 
{
    protected $casts = [
        'email' => 'encrypted:deterministic',
        'name' => 'encrypted',
    ];
}

$user = User::create([
        'email' => 'user@example.com',
        'name' => 'John Doe',
);


User::where('email', encrypt('user@example.com', false, true))->first()

Security Considerations

Deterministic encryption has inherent trade-offs: Pros:

  • Enables searchable encryption and exact-match queries
  • Maintains data integrity for deduplication
  • Useful for creating consistent encrypted indexes

Cons:

  • Identical plaintexts produce identical ciphertexts (by design)
  • May reveal patterns in encrypted data
  • Not suitable for all types of sensitive data

Recommendations:

  • Use deterministic encryption only when necessary
  • Avoid for highly sensitive data where pattern analysis is a concern
  • Consider adding additional entropy (salts) at the application level when appropriate
  • Always use HTTPS/TLS for data in transit

Testing

Comprehensive test coverage has been added to verify:

  • Deterministic encryption produces consistent results
  • Non-deterministic encryption produces random results
  • Cross-compatibility between both modes
  • Proper decryption of both deterministic and non-deterministic data
  • Behavior with different cipher algorithms (CBC and GCM)
  • Edge cases with empty strings and large data sets
  • Eloquent cast functionality for all deterministic cast types
  • Proper serialization and deserialization with deterministic casts

Breaking Changes

None. This is a purely additive change that maintains full backward compatibility.

@mostafamaklad mostafamaklad reopened this Jul 1, 2025
@mostafamaklad mostafamaklad changed the base branch from master to 12.x July 1, 2025 19:27
Copy link
Member

@GrahamCampbell GrahamCampbell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. I don't speak on behalf of Laravel with this feedback, just me personally - I think this use case is quite fringe and doesn't belong in the framework core. It would make a fine package, and people who need this can pull it into their app.

* @throws \Illuminate\Contracts\Encryption\DecryptException
*/
public function decrypt($payload, $unserialize = true);
public function decrypt(string $payload, bool $unserialize = true): mixed;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes to the contracts are major breaking changes, and cannot be accepted on 12.x.

Copy link
Author

@mostafamaklad mostafamaklad Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can push it to laravel 13 as new feature

@mostafamaklad
Copy link
Author

Thanks for the PR. I don't speak on behalf of Laravel with this feedback, just me personally - I think this use case is quite fringe and doesn't belong in the framework core. It would make a fine package, and people who need this can pull it into their app.

This is a must for encrypting something searchable like email or phone you should use deterministic. if you want to implement the PII we can extend as well the encrypted casting

All test cases ran successfully.
You can check it

@mostafamaklad mostafamaklad reopened this Jul 1, 2025
@mostafamaklad
Copy link
Author

Now covering deterministic encryption casting as well

@iBotPeaches
Copy link
Contributor

This is a must for encrypting something searchable like email or phone you should use deterministic. if you want to implement the PII we can extend as well the encrypted casting

That is one way to be clear. You can use a hash column commonly known as "blind indexing" alongside the non-deterministic encrypted blob to make fields searchable with the caveat that you won't get any partial searches - more so for where and whereIn direct searches. Just be sure you aren't using the same key to hmac that you used to encrypt.

I don't think throwing an additional parameter on a function is the best developer experience for something that drastically changes the security footprint of a method. I'm with Graham that this probably doesn't belong in core.

@taylorotwell
Copy link
Member

Thanks for your pull request to Laravel!

Unfortunately, I'm going to delay merging this code for now. To preserve our ability to adequately maintain the framework, we need to be very careful regarding the amount of code we include.

If applicable, please consider releasing your code as a package so that the community can still take advantage of your contributions!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants