spatie/laravel-ciphersweet
Laravel wrapper for Paragonie CipherSweet that adds searchable field-level encryption to Eloquent models. Encrypt/decrypt sensitive attributes and generate blind indexes so you can query encrypted data securely without exposing readable values in your database.
Installation
composer require spatie/laravel-ciphersweet
php artisan vendor:publish --tag="ciphersweet-migrations"
php artisan migrate
Publish config (optional):
php artisan vendor:publish --tag="ciphersweet-config"
Configure Model
Add UsesCipherSweet trait and implement CipherSweetEncrypted:
use Spatie\LaravelCipherSweet\Concerns\UsesCipherSweet;
use Spatie\LaravelCipherSweet\Contracts\CipherSweetEncrypted;
class User implements CipherSweetEncrypted {
use UsesCipherSweet;
public static function configureCipherSweet(EncryptedRow $encryptedRow): void {
$encryptedRow
->addField('email')
->addBlindIndex('email', new BlindIndex('email_index'));
}
}
Generate & Set Key
php artisan ciphersweet:generate-key
Add to .env:
CIPHERSWEET_KEY=<generated-key>
Encrypt Existing Data
php artisan ciphersweet:encrypt App\Models\User
Encrypt a new user's email:
$user = User::create(['email' => 'user@example.com']);
// Automatically encrypted via model events
Model Setup
configureCipherSweet():
$encryptedRow
->addField('ssn') // Encrypts as text
->addBooleanField('is_active')
->addBlindIndex('ssn', new BlindIndex('ssn_index', 128));
Database Schema
text columns for encrypted fields:
Schema::table('users', function (Blueprint $table) {
$table->text('ssn')->nullable();
});
Searching
$users = User::whereBlind('ssn', 'ssn_index', '123-45-6789')->get();
Validation
$request->validate([
'email' => [Rule::encryptedUnique(User::class, 'email_index')],
]);
Laravel Events: Leverage saving, updating events to auto-encrypt:
protected static function booted(): void {
static::saving(function ($model) {
$model->encrypt();
});
}
API Responses: Decrypt before returning:
$user = User::find(1);
return response()->json(['email' => $user->email]); // Auto-decrypted
Batch Processing: Use chunking for large datasets:
User::chunk(100, function ($users) {
foreach ($users as $user) {
$user->encrypt();
$user->save();
}
});
Key Rotation:
php artisan ciphersweet:generate-key
php artisan ciphersweet:encrypt App\Models\User --new-key=<new-key>
Migration Order
ciphersweet:encrypt after migrations. The package adds blind_indexes table automatically.Field Types
text for addField()).addOptional*Field() to skip encryption for NULL values.Key Management
Performance
Validation Quirks
EncryptedUniqueRule requires the model to implement CipherSweetEncrypted and define the blind index.Encryption Failures:
CIPHERSWEET_KEY in .env is correct.nacl, boring, etc.) is supported by your PHP version.Search Issues:
configureCipherSweet() and queries:
// Wrong: Non-existent index
User::whereBlind('email', 'wrong_index', 'test@example.com');
// Correct:
User::whereBlind('email', 'email_index', 'test@example.com');
Key Rotation Errors:
php artisan ciphersweet:encrypt with --force to re-encrypt all data:
php artisan ciphersweet:encrypt App\Models\User --force
Custom Backends
config([
'ciphersweet.backend' => 'custom',
'ciphersweet.backend.custom' => \App\Providers\CustomBackendFactory::class,
]);
Key Providers
config([
'ciphersweet.provider' => 'custom',
'ciphersweet.providers.custom' => \App\Providers\ApiKeyProvider::class,
]);
Field Maps for JSON
$fieldMap = [
'address.city' => 'text',
'address.zip' => 'text',
];
$encryptedRow->addJsonField('metadata', $fieldMap);
Query Scopes
public function scopeWhereEmailContains($query, string $term) {
// Note: Blind indexes don't support partial matches.
// This is a placeholder for custom logic.
}
Testing:
CIPHERSWEET_PROVIDER=random in .env for ephemeral keys in tests.UsesCipherSweet trait for unit tests:
$model->shouldReceive('encrypt')->once();
Monitoring:
event(new KeyRotated($oldKey, $newKey));
Legacy Data:
--skip-existing to avoid re-encrypting already-processed records:
php artisan ciphersweet:encrypt App\Models\User --skip-existing
How can I help you explore Laravel packages today?