laravel-ciphersweet

repository·main·Indexed 19 days ago

https://github.com/spatie/laravel-ciphersweet

A Laravel wrapper for the CipherSweet library that provides Eloquent integration for field-level encryption and searchable blind indexes. It includes tools for generating encryption keys, rotating keys via the ciphersweet:encrypt command, and performing exact-match searches using whereBlind and orWhereBlind scopes. The package also provides an EncryptedUniqueRule and a corresponding Laravel validation macro to validate uniqueness on encrypted columns.

Tokens
4.7K
Snippets
18
Records
20
Agent score
53%

What's inside laravel-ciphersweet

  1. Rotate encryption keys

    main

    If a key is compromised, you can rotate it by:

    1. Generating a new key using php artisan ciphersweet:generate-key.
    2. Running the encryption command with the new key: php artisan ciphersweet:encrypt "App\User" <your-new-key>
    3. Updating your .env or config file to use the new key.

    This process updates all encrypted fields and blind indexes for the specified model.

    php artisan ciphersweet:encrypt "App\User" <your-new-key>
  2. Implement a custom backend

    main

    To use a custom encryption backend, set the ciphersweet.backend configuration value to custom. You must then provide an invokeable factory class in the ciphersweet.backend.custom configuration key. This factory must return an object implementing ParagonIE\CipherSweet\Contract\BackendInterface.

    Your implementation of BackendInterface must define the logic for encryption, decryption, blind indexing (both fast and slow), key derivation, and streaming operations.

    // 1. Define your backend implementation
    class CustomBackend implements BackendInterface {
        public function encrypt(string $plaintext, SymmetricKey $key, string $aad = ''): string { /* ... */ }
        public function decrypt(string $ciphertext, SymmetricKey $key, string $aad = ''): string { /* ... */ }
        public function blindIndexFast(string $plaintext, SymmetricKey $key, ?int $bitLength = null): string { /* ... */ }
        public function blindIndexSlow(string $plaintext, SymmetricKey $key, ?int $bitLength = null, array $config = []): string { /* ... */ }
        public function getIndexTypeColumn(string $tableName, string $fieldName, string $indexName): string { /* ... */ }
        public function deriveKeyFromPassword(string $password, string $salt): SymmetricKey { /* ... */ }
        public function doStreamDecrypt($inputFP, $outputFP, SymmetricKey $key, int $chunkSize = 8192, ?AAD $aad = null): bool { /* ... */ }
        public function doStreamEncrypt($inputFP, $outputFP, SymmetricKey $key, int $chunkSize = 8192, string $salt = Constants::DUMMY_SALT, ?AAD $aad = null): bool { /* ... */ }
        public function getFileEncryptionSaltOffset(): int { /* ... */ }
        public function getPrefix(): string { /* ... */ }
    }
    
    // 2. Create the factory
    class CustomBackendFactory {
        public function __invoke()
        {
            return new CustomBackend();
        }
    }
    
    // 3. In your config file:
    // 'ciphersweet.backend' => 'custom',
    // 'ciphersweet.backend.custom' => CustomBackendFactory::class,
  3. Prepare an Eloquent model for encryption

    main

    To encrypt model attributes, your model must implement the Spatie\LaravelCipherSweet\Contracts\CipherSweetEncrypted interface and use the Spatie\LaravelCipherSweet\Concerns\UsesCipherSweet trait.

    You must implement the configureCipherSweet method to define which fields are encrypted and which have blind indexes for searching.

    Field Types:

    • addField('column_name'): Standard encrypted field.
    • addBooleanField('column_name'), addIntegerField('column_name'), addTextField('column_name'): Type-specific encrypted fields.
    • addOptional...('column_name'): Fields that remain unencrypted NULL if a NULL value is provided.
    • addJsonField('column_name', $fieldMap): Encrypts a JSON array using a field map.
    • addBlindIndex('column_name', new BlindIndex('index_name')): Creates a blind index for exact-match searching on the column.

    Note: Database columns used for encrypted values should be of type text.

    use Spatie\LaravelCipherSweet\Contracts\CipherSweetEncrypted;
    use Spatie\LaravelCipherSweet\Concerns\UsesCipherSweet;
    use ParagonIE\CipherSweet\EncryptedRow;
    use ParagonIE\CipherSweet\BlindIndex;
    use Illuminate\Database\Eloquent\Model;
    
    class User extends Model implements CipherSweetEncrypted
    {
        use UsesCipherSweet;
        
        public static function configureCipherSweet(EncryptedRow $encryptedRow): void
        {
            $encryptedRow
                ->addField('email')
                ->addBlindIndex('email', new BlindIndex('email_index'));
        }
    }
  4. Implement a custom key provider

    main

    To use a custom key provider, set the ciphersweet.provider configuration value to custom. You must then provide an invokeable factory class in the ciphersweet.providers.custom configuration key. This factory must return an object implementing ParagonIE\CipherSweet\Contract\KeyProviderInterface.

    The provider must implement the getSymmetricKey() method, which returns a SymmetricKey instance used for encryption and decryption operations.

    // 1. Define your key provider implementation
    class CustomKeyProvider implements KeyProviderInterface {
        public function getSymmetricKey(): SymmetricKey
        {
            return new SymmetricKey('your-key-here');
        }
    }
    
    // 2. Create the factory
    class CustomKeyProviderFactory {
        public function __invoke()
        {
            return new CustomKeyProvider();
        }
    }
    
    // 3. In your config file:
    // 'ciphersweet.provider' => 'custom',
    // 'ciphersweet.providers.custom' => CustomKeyProviderFactory::class,
  5. Install Laravel CipherSweet

    main

    Install the package via Composer, then publish and run the migrations to set up the necessary database tables (such as blind_indexes).

    composer require spatie/laravel-ciphersweet
    php artisan vendor:publish --tag="ciphersweet-migrations"
    php artisan migrate
  6. Configure the CipherSweet config file

    main

    You can publish the configuration file using the ciphersweet-config tag. The configuration allows you to define the cryptographic backend, the key provider, and provider-specific options like file paths or string keys.

    Key configuration options:

    • backend: The cryptographic backend to use. Recommended: nacl. Supported: boring, fips, nacl.
    • provider: How the key is retrieved. Supported: file, random, string.
    • providers: Settings for specific providers (e.g., path for file or key for string).
    • permit_empty: Boolean determining if empty fields should throw an EmptyFieldException.
    return [
        'backend' => env('CIPHERSWEET_BACKEND', 'nacl'),
        'provider' => env('CIPHERSWEET_PROVIDER', 'string'),
        'providers' => [
            'file' => [
                'path' => env('CIPHERSWEET_FILE_PATH'),
            ],
            'string' => [
                'key' => env('CIPHERSWEET_KEY'),
            ],
        ],
        'permit_empty' => env('CIPHERSWEET_PERMIT_EMPTY', FALSE)
    ];
  7. Validate encrypted field uniqueness

    main

    Standard Laravel Rule::unique() does not work on encrypted columns. Use EncryptedUniqueRule or the Rule::encryptedUnique() macro to validate uniqueness via blind indexes.

    Usage via Macro:

    use Illuminate\Validation\Rule;
    
    $request->validate([
        'email' => [Rule::encryptedUnique(User::class, 'email_index')],
    ]);

    Usage via Class:

    use Spatie\LaravelCipherSweet\Rules\EncryptedUniqueRule;
    
    $request->validate([
        'email' => [new EncryptedUniqueRule(User::class, 'email_index')],
    ]);

    Options:

    • Column Name: The third parameter defaults to the validation attribute name (e.g., email).
    • Ignoring Records: Use ignore($id) or ignoreModel($model) to skip a specific record during updates.
    Rule::encryptedUnique(User::class, 'email_index')->ignore($user->id)
  8. Search on blind indexes

    main

    Even with encrypted data, you can perform exact-match searches using the whereBlind and orWhereBlind Eloquent scopes.

    Parameters:

    1. column: The database column name.
    2. indexName: The name of the blind index defined in configureCipherSweet.
    3. value: The raw value to search for (the package handles hashing/transformations).
    $user = User::whereBlind('email', 'email_index', 'rias@spatie.be');
  9. Configure CipherSweet backend and key provider

    main

    The package uses a configuration file to determine which cryptographic backend and key provider to use.

    Backends

    Available backends via ciphersweet.backend:

    • fips: Uses FIPSCrypto.
    • boring: Uses BoringCrypto.
    • modern (default): Uses ModernCrypto.
    • custom: Allows you to specify a custom class in ciphersweet.backends.custom. The class must implement ParagonIE\CipherSweet\Contract\BackendInterface.

    Key Providers

    Available providers via ciphersweet.provider:

    • file: Uses FileProvider with a path defined in ciphersweet.providers.file.path.
    • string: Uses StringProvider with a key defined in ciphersweet.providers.string.key.
    • random (default): Uses RandomProvider.
    • custom: Allows you to specify a custom class in ciphersweet.providers.custom. The class must implement ParagonIE\CipherSweet\Contract\KeyProviderInterface.
  10. Encrypt existing model attributes

    main

    To encrypt existing values in your database for a specific model, run the ciphersweet:encrypt command. This command updates both encrypted fields and their corresponding blind indexes. It is restartable, meaning you can re-run it without re-encrypting already rotated keys.

    php artisan ciphersweet:encrypt <your-model-class> <generated-key>
    php artisan ciphersweet:encrypt "App\User" <your-new-key>
  11. Use the EncryptedUniqueRule for unique validation on encrypted columns

    main

    The EncryptedUniqueRule allows you to perform unique validation on columns that are encrypted using CipherSweet. It works by querying the blind index associated with the column rather than the encrypted data itself.

    To use it, you must provide the model class, the name of the blind index, and optionally the column name. The model being validated must implement the Spatie\LaravelCipherSweet\Contracts\CipherSweetEncrypted interface.

    Ignoring records during validation

    When updating an existing record, you must ignore its current ID so the validation doesn't fail against itself. You can use the ignore() method with a raw ID or the ignoreModel() method with an Eloquent model instance.

    use Spatie\LaravelCipherSweet\Rules\EncryptedUniqueRule;
    
    // Basic usage
    'email' => [new EncryptedUniqueRule(User::class, 'email_index')],
    
    // Usage with ignoring an existing model (e.g., during an update)
    'email' => [new EncryptedUniqueRule(User::class, 'email_index')->ignore($user)],
    
    // Usage with ignoring a specific ID
    'email' => [new EncryptedUniqueRule(User::class, 'email_index')->ignore(123)],