paperscissorsandglue/laravel-encryption-at-rest
Install the package with Composer:
composer require paperscissorsandglue/laravel-encryption-at-rest
Publish the config and migrations:
php artisan vendor:publish --tag=encryption-at-rest-config
php artisan vendor:publish --tag=encryption-at-rest-migrations # for email_index support
Add encryption to your models:
use Paperscissorsandglue\EncryptionAtRest\Encryptable;use Paperscissorsandglue\EncryptionAtRest\EncryptableJson;use Paperscissorsandglue\EncryptionAtRest\HasEncryptedEmail;First use case: Encrypt a phone and address field on a User model:
class User extends Model
{
use Encryptable;
protected $encryptable = ['phone', 'address'];
}
Save data like normal — encryption happens automatically on persist.
Trait-based adoption
Apply Encryptable to existing models incrementally. Start with non-critical fields (e.g., phone) before moving to PII like ssn or bank_account.
JSON encryption in fragments
When using $encryptableJson, encrypt only specific top-level keys inside JSON columns (e.g., preferences.notification_email). Keep non-sensitive keys (e.g., theme, language) unencrypted for querying.
Encrypted email authentication
email_index column (via migration).Authenticatable provider to 'driver' => 'encrypted-email'.email_index:
php artisan encryption:encrypt-emails "App\Models\User"
Batch migration of legacy data
For pre-existing records:
php artisan encryption:encrypt-model "App\Models\Order" --filter="payment_status = 'pending'" --chunk=500
Direct service usage
Use EncryptionService in non-Eloquent contexts (e.g., caching, queue payloads):
$this->encryptionService->encrypt($creditCardToken);
Double encryption risk
⚠️ Critical: Since v0.2.0, encryption now occurs on assignment (not save). Avoid reassigning decrypted values (e.g., $user->email = $user->email;) as this can cause double encryption. Use getAttribute() or clone to prevent re-encryption.
Email uniqueness caveat
email_index is deterministic (SHA256). Two users cannot share the same email, but the index itself is not a security risk — it’s a one-way hash. Still, ensure your email_index column is unique: true.
No WHERE on encrypted fields
Standard Eloquent queries like User::where('phone', '555') will fail — only findByEmail() works for encrypted email. For other fields, store a searchable hash or use application-layer filtering.
Attribute casting conflicts
Don’t define encrypted fields as casts => ['phone' => 'string'] — let the trait’s accessor/mutator handle type conversion.
Relationships & serialization
Relationships work normally. However, when using toArray() or JSON responses, ensure no raw encrypted data leaks (it won’t — the trait re-decrypts on access).
CLI safety
Always use --dry-run first with migration commands. Default backups are helpful but not foolproof — ensure external backups exist too.
Key management
ENCRYPTION_AT_REST_KEY defaults to APP_KEY. Never rotate APP_KEY post-deployment — if you must, use EncryptionService + custom key rotation workflow to re-encrypt existing data.
Testing
Use factories that set sensitive fields as plain text — e.g., ->state(['phone' => '+1-555-1234']). The trait handles encryption transparently during save().
Testing encrypted email login
Test with Auth::attempt(['email' => 'test@example.com', 'password' => '...']) — the encrypted-email provider handles lookups via email_index. Ensure remember_token and sessions work after enabling.
How can I help you explore Laravel packages today?