PHPackages                             vimatech-io/laravel-secure-fields - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. vimatech-io/laravel-secure-fields

Abandoned → [vimatech/laravel-secure-fields](/?search=vimatech%2Flaravel-secure-fields)Library[Database &amp; ORM](/categories/database)

vimatech-io/laravel-secure-fields
=================================

Secure encrypted Eloquent model fields for sensitive data in Laravel applications.

v1.0.2(1mo ago)01↑2900%MITPHPPHP ^8.3CI passing

Since Jun 5Pushed 4w agoCompare

[ Source](https://github.com/vimatech-io/laravel-secure-fields)[ Packagist](https://packagist.org/packages/vimatech-io/laravel-secure-fields)[ Docs](https://github.com/vimatech-io/laravel-secure-fields)[ RSS](/packages/vimatech-io-laravel-secure-fields/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (9)Versions (4)Used By (0)

Laravel Secure Fields
=====================

[](#laravel-secure-fields)

[![Latest Version on Packagist](https://camo.githubusercontent.com/5411ef80fd328ab7f7a0f7be5e50a9452645bbcaedf86e56f899271eba8a9a8e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f76696d61746563682f6c61726176656c2d7365637572652d6669656c64732e737667)](https://packagist.org/packages/vimatech/laravel-secure-fields)[![CI](https://github.com/vimatech-io/laravel-secure-fields/actions/workflows/ci.yml/badge.svg)](https://github.com/vimatech-io/laravel-secure-fields/actions/workflows/ci.yml)[![PHPStan](https://camo.githubusercontent.com/b6d441ad4fe8332cb16c72aa27f22cc685181dfd74ae34964afc92c6c1146b3c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c2532306d61782d627269676874677265656e2e737667)](https://phpstan.org/)[![License](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)](LICENSE.md)

**Secure encrypted Eloquent model fields for Laravel.**

Laravel Secure Fields lets you encrypt sensitive database fields with AES-256-GCM while preserving a natural Eloquent developer experience — searchable, maskable, and rotatable.

Why Laravel Secure Fields?
--------------------------

[](#why-laravel-secure-fields)

Most Laravel apps storing sensitive data eventually need to answer:

- How do I encrypt PII fields at rest?
- How do I search encrypted data without decrypting everything?
- How do I rotate keys without downtime?
- How do I prevent accidental plaintext leaks in API responses?

Laravel Secure Fields provides a focused encryption layer for that.

Feature Matrix
--------------

[](#feature-matrix)

FeatureSupportedAES-256-GCM encryption✅Random IV per encryption✅Auth tag validation✅Searchable encrypted fields (blind index)✅Key rotation command✅Field masking✅Encrypted JSON fields✅Serialization protection✅Audit logging✅Facade✅LIKE / partial search❌ (by design)Homomorphic encryption❌UI components❌Laravel Secure Fields vs Laravel's Built-in Encryption
------------------------------------------------------

[](#laravel-secure-fields-vs-laravels-built-in-encryption)

Laravel Secure Fields manages:

- **Field-level** encryption with proper AES-256-GCM
- **Searchable** encrypted fields via blind indexes
- **Key rotation** tooling
- **Serialization safety** and masking

Laravel's `encrypt()` / `Crypt` facade:

- General-purpose encryption
- No searchability
- No field-level tooling
- No rotation command

They are complementary — this package is purpose-built for Eloquent model fields.

Use Cases
---------

[](#use-cases)

- PII storage (SSN, phone, email)
- GDPR / HIPAA compliance
- Payment-related data
- API keys and secrets storage
- Healthcare records
- Legal documents
- Multi-tenant sensitive data

Installation
------------

[](#installation)

### Requirements

[](#requirements)

- PHP 8.3+
- Laravel 11, 12, or 13
- OpenSSL extension

```
composer require vimatech/laravel-secure-fields
```

### Publish config

[](#publish-config)

```
php artisan vendor:publish --tag=secure-fields-config
```

### Publish migrations (optional, for audit logging)

[](#publish-migrations-optional-for-audit-logging)

```
php artisan vendor:publish --tag=secure-fields-migrations
php artisan migrate
```

Generating Keys
---------------

[](#generating-keys)

> **Important:** Always generate dedicated keys. Never leave `SECURE_FIELDS_KEY` or `SECURE_FIELDS_HASH_KEY` empty — an empty value silently falls back to a key derived from `APP_KEY`, which means a compromised `APP_KEY` would expose both session/cookie encryption **and** all field-level ciphertext.

Generate a 32-byte encryption key (base64-encoded):

```
php -r "echo base64_encode(random_bytes(32)), PHP_EOL;"
```

Generate a 32-byte hash key (hex-encoded or base64):

```
php -r "echo bin2hex(random_bytes(32)), PHP_EOL;"
```

Add both to your `.env`:

```
SECURE_FIELDS_KEY=
SECURE_FIELDS_HASH_KEY=
```

**Minimum requirements:**

- `SECURE_FIELDS_KEY` — 32 bytes, base64-encoded (validated at boot)
- `SECURE_FIELDS_HASH_KEY` — minimum 32 bytes/characters (validated at boot)

Quick Start
-----------

[](#quick-start)

### 1. Add encrypted fields to your model

[](#1-add-encrypted-fields-to-your-model)

```
use VimaTech\SecureFields\Casts\SecureField;
use VimaTech\SecureFields\Casts\SecureJson;
use VimaTech\SecureFields\Traits\HasSecureFields;

class User extends Model
{
    use HasSecureFields;

    protected $casts = [
        'email'    => SecureField::class,
        'phone'    => SecureField::class,
        'ssn'      => SecureField::class,
        'metadata' => SecureJson::class,
    ];

    protected array $secureSearchable = [
        'email',
        'phone',
    ];
}
```

### 2. Create your migration

[](#2-create-your-migration)

```
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->text('email');                       // required field — not nullable
    $table->string('email_hash', 64);            // blind index for searching
    $table->text('phone')->nullable();           // optional field
    $table->string('phone_hash', 64)->nullable();
    $table->text('ssn');
    $table->text('metadata')->nullable();        // optional JSON field
    $table->timestamps();
});
```

> **Important:** Use `TEXT` columns for encrypted fields — encrypted payloads are larger than plaintext. Add `nullable()` only when the field is genuinely optional in your domain. The cast handles `null` values correctly in both cases.

### 3. Use it naturally

[](#3-use-it-naturally)

```
// Create — automatically encrypted
$user = User::create([
    'email' => 'john@example.com',
    'phone' => '+1234567890',
    'ssn'   => '123-45-6789',
    'metadata' => ['plan' => 'premium', 'preferences' => ['dark_mode' => true]],
]);

// Read — automatically decrypted
echo $user->email; // "john@example.com"

// The database stores encrypted ciphertext — never plaintext
```

Searchable Encrypted Fields
---------------------------

[](#searchable-encrypted-fields)

Search encrypted fields without exposing plaintext:

```
// Exact-match search on encrypted field
$user = User::secureWhere('email', 'john@example.com')->first();

// Chain with other queries
$users = User::secureWhere('phone', '+1234567890')
    ->where('active', true)
    ->get();
```

The package stores a deterministic HMAC-SHA256 hash alongside the encrypted value, enabling exact-match queries while the actual data remains encrypted.

### How it works

[](#how-it-works)

1. On save: encrypts the value AND stores `HMAC-SHA256(plaintext)` in a `{field}_hash` column
2. On search: hashes the search term and queries the hash column
3. The hash is one-way — it cannot be reversed to obtain the plaintext

### Search normalization

[](#search-normalization)

Blind index hashes are **case-insensitive** and **whitespace-trimmed**. These three searches are equivalent and will find the same record:

```
User::secureWhere('email', 'John@Example.COM')
User::secureWhere('email', '  john@example.com  ')
User::secureWhere('email', 'john@example.com')
```

This normalization is applied consistently on both write and search, so records are always findable regardless of input case.

Field Masking
-------------

[](#field-masking)

Encrypted fields are **hidden by default** from `toArray()` and `toJson()` to prevent accidental exposure. `toMaskedArray()` makes them visible with masking applied:

```
$user->masked('phone');       // "********7890"
$user->masked('ssn');         // "*******6789"
$user->masked('phone', 2);    // "**********90"

// Returns all model fields with secure fields replaced by masked values
$user->toMaskedArray();       // ['id' => 1, 'phone' => '********7890', ...]
```

You can also mask individual fields with custom parameters:

```
$user->masked('phone', visibleEnd: 4, maskChar: '#'); // "########7890"
```

Encrypted JSON Fields
---------------------

[](#encrypted-json-fields)

Encrypt entire JSON structures:

```
protected $casts = [
    'metadata' => SecureJson::class,
];

// Works like a normal JSON cast, but encrypted at rest
$user->metadata = ['api_key' => 'sk_live_...', 'tokens' => 42];
$user->save();

echo $user->metadata['api_key']; // "sk_live_..."
```

Key Rotation
------------

[](#key-rotation)

Rotate encryption keys without downtime. The rotation command re-encrypts all field values with the new key while the `SECURE_FIELDS_KEY` in your `.env` already points to the new key.

### Rotation workflow

[](#rotation-workflow)

1. Generate a new key: `php -r "echo base64_encode(random_bytes(32)), PHP_EOL;"`
2. Update `SECURE_FIELDS_KEY` in `.env` to the new key
3. Run the rotation command with the **old** key
4. Verify data integrity
5. Remove the old key from any backups or records

### Running the rotation

[](#running-the-rotation)

The old key is read via a **secure interactive prompt** that does not appear in process listings or shell history:

```
# Interactive prompt (recommended — key never appears in shell history)
php artisan secure-fields:rotate "App\Models\User"
# > Enter the old encryption key (base64): [hidden input]

# Or pass via --old-key for automated scripts
# WARNING: --old-key is visible in `ps aux` and shell history
php artisan secure-fields:rotate "App\Models\User" --old-key=BASE64_OLD_KEY

# Preview without persisting
php artisan secure-fields:rotate "App\Models\User" --dry-run

# Rotate specific fields
php artisan secure-fields:rotate "App\Models\User" \
    --fields=email \
    --fields=phone \
    --chunk=1000
```

> **Security note:** For automated pipelines, prefer passing the old key via an environment variable read inside a wrapper script rather than as a CLI argument:
>
> ```
> OLD_KEY=$(vault kv get -field=old_key secret/secure-fields)
> php artisan secure-fields:rotate "App\Models\User" --old-key="$OLD_KEY"
> ```

### Hash key rotation

[](#hash-key-rotation)

The `SECURE_FIELDS_HASH_KEY` is **separate** from the encryption key and used only for HMAC blind indexes. If you need to rotate the hash key:

1. Changing `SECURE_FIELDS_HASH_KEY` will invalidate all existing blind indexes — `secureWhere()` queries will return no results for existing records until indexes are rebuilt.
2. A `secure-fields:rehash` command for rebuilding indexes is planned for a future release.
3. Until then, rotate the hash key only during a maintenance window where you can rebuild indexes manually.

Serialization Protection
------------------------

[](#serialization-protection)

Encrypted fields are automatically hidden from `toArray()` and `toJson()` to prevent accidental exposure in API responses or logs:

```
$user->toArray();        // email, phone, ssn are excluded
$user->toSecureArray();  // same — always excludes all encrypted fields
$user->toMaskedArray();  // includes masked versions of encrypted fields
```

Audit Logging
-------------

[](#audit-logging)

The package can log every field decryption event, enabling access trail for GDPR, HIPAA, and SOC 2 compliance.

### Configuration

[](#configuration)

```
SECURE_FIELDS_AUDIT=true
SECURE_FIELDS_AUDIT_DRIVER=database   # 'database' or 'log'
SECURE_FIELDS_AUDIT_CHANNEL=stack     # Laravel log channel (for 'log' driver)
```

### What is logged

[](#what-is-logged)

EventTriggerRecorded fields`decrypt`Reading an encrypted attributemodel, model\_id, field, user\_id, action, ip\_address, user\_agent`key_rotation``secure-fields:rotate` completesmodel, records\_processed, user\_id, action, ip\_address, user\_agent### Deduplication

[](#deduplication)

Within a single request, the same `(model, id, field)` combination is logged **at most once**, regardless of how many times the attribute is accessed. This prevents log flooding when iterating over collections.

### Database driver

[](#database-driver)

With `SECURE_FIELDS_AUDIT_DRIVER=database`, audit rows are **batched and written in a single INSERT** at the end of the request — not one INSERT per access. This keeps the hot path free of synchronous database writes.

The audit table must be published and migrated before enabling the database driver:

```
php artisan vendor:publish --tag=secure-fields-migrations
php artisan migrate
```

### FrankenPHP / Laravel Octane

[](#frankenphp--laravel-octane)

The `AuditLogger` is bound as `scoped()` in the service container, meaning a fresh instance is created for each request in Octane and FrankenPHP worker mode. The deduplication cache and pending batch are request-scoped and never leak between requests.

### Log driver

[](#log-driver)

The `log` driver writes to a Laravel log channel with no additional database queries — a good default for high-throughput applications.

```
SECURE_FIELDS_AUDIT=true
SECURE_FIELDS_AUDIT_DRIVER=log
SECURE_FIELDS_AUDIT_CHANNEL=daily
```

Facade Usage
------------

[](#facade-usage)

```
use VimaTech\SecureFields\Facades\SecureFields;

// Encrypt/decrypt manually
$encrypted = SecureFields::encrypt('sensitive data');
$decrypted = SecureFields::decrypt($encrypted);

// Hash for searching
$hash = SecureFields::hash('john@example.com');
$matches = SecureFields::verifyHash('john@example.com', $hash); // true
```

Configuration
-------------

[](#configuration-1)

```
// config/secure-fields.php

return [
    // Base64-encoded 32-byte encryption key.
    // REQUIRED in production — see "Generating Keys" section.
    // Falls back to HKDF derivation from APP_KEY if not set (not recommended).
    'key' => env('SECURE_FIELDS_KEY'),

    'cipher' => 'aes-256-gcm',

    'hashing' => [
        // Minimum 32 bytes. REQUIRED in production.
        // Falls back to HKDF derivation from APP_KEY if not set (not recommended).
        'key'       => env('SECURE_FIELDS_HASH_KEY'),
        'algorithm' => 'sha256',
    ],

    'rotation' => [
        'chunk_size' => 500,
        'queue'      => env('SECURE_FIELDS_QUEUE'),
        'connection' => env('SECURE_FIELDS_QUEUE_CONNECTION'),
    ],

    'masking' => [
        'character'   => '*',
        'visible_end' => 4,
    ],

    'audit' => [
        'enabled'     => env('SECURE_FIELDS_AUDIT', false),
        'driver'      => env('SECURE_FIELDS_AUDIT_DRIVER', 'log'), // 'database' or 'log'
        'log_channel' => env('SECURE_FIELDS_AUDIT_CHANNEL', 'stack'),
    ],
];
```

Environment Variables
---------------------

[](#environment-variables)

```
# Encryption key — 32 bytes, base64-encoded (REQUIRED)
SECURE_FIELDS_KEY=

# Hash key for blind indexes — minimum 32 bytes (REQUIRED)
SECURE_FIELDS_HASH_KEY=

# Audit logging
SECURE_FIELDS_AUDIT=false
SECURE_FIELDS_AUDIT_DRIVER=log
SECURE_FIELDS_AUDIT_CHANNEL=stack
```

Complete Example
----------------

[](#complete-example)

```
use VimaTech\SecureFields\Casts\SecureField;
use VimaTech\SecureFields\Casts\SecureJson;
use VimaTech\SecureFields\Traits\HasSecureFields;

// 1. Define your model
class User extends Model
{
    use HasSecureFields;

    protected $casts = [
        'email'    => SecureField::class,
        'phone'    => SecureField::class,
        'ssn'      => SecureField::class,
        'metadata' => SecureJson::class,
    ];

    protected array $secureSearchable = ['email', 'phone'];
}

// 2. Use it naturally
$user = User::create([
    'email'    => 'john@example.com',
    'phone'    => '+1234567890',
    'ssn'      => '123-45-6789',
    'metadata' => ['plan' => 'premium'],
]);

$user->email;           // "john@example.com" (decrypted)
$user->masked('phone'); // "********7890"
$user->masked('ssn');   // "*******6789"

// 3. Search encrypted fields
User::secureWhere('email', 'john@example.com')->first();
User::secureWhere('email', 'JOHN@EXAMPLE.COM')->first(); // same result — case-insensitive

// 4. Serialization is safe by default
$user->toArray();       // email, phone, ssn excluded
$user->toMaskedArray(); // ['id' => 1, 'email' => '**************com', ...]
```

Security Notes
--------------

[](#security-notes)

### Encryption

[](#encryption)

- Uses **AES-256-GCM** — authenticated encryption providing confidentiality and integrity
- Every encryption generates a **unique random 12-byte IV** — no IV reuse
- **16-byte authentication tags** protect against tampering
- Keys are derived via **HKDF** when using the `APP_KEY` fallback (not recommended for production)

### Key Management

[](#key-management)

- Always set **dedicated** `SECURE_FIELDS_KEY` and `SECURE_FIELDS_HASH_KEY` values
- The `APP_KEY` fallback couples your session/cookie security to your field encryption — a compromised `APP_KEY` exposes both
- Store keys in a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) rather than `.env` files for production

### Searchable Fields

[](#searchable-fields)

- Uses **HMAC-SHA256** with a separate key for blind indexes
- Hash indexes enable **exact-match only** — no partial search, no LIKE queries
- The hash is **deterministic** but **one-way** — cannot be reversed to plaintext
- Uses **constant-time comparison** to prevent timing attacks
- Search values are **normalized** (lowercased, trimmed) before hashing — ensure data is stored with the same normalization

### Best Practices

[](#best-practices)

- Use a dedicated `SECURE_FIELDS_KEY` separate from `APP_KEY`
- Use a dedicated `SECURE_FIELDS_HASH_KEY` for search indexes
- Rotate encryption keys periodically
- Enable audit logging in production (`SECURE_FIELDS_AUDIT=true`)
- Use `TEXT` columns — encrypted data is larger than plaintext
- Add `nullable()` only when the field is genuinely optional in your domain
- Never log decrypted sensitive values
- Configure `TrustProxies` middleware for accurate IP logging in audit records

Philosophy
----------

[](#philosophy)

Laravel Secure Fields is intentionally focused.

The package manages:

- **Encryption** at the field level
- **Searchability** via blind indexes
- **Key rotation** tooling
- **Serialization safety**

Design principles:

- Backend-only, UI agnostic
- Security-first defaults
- Laravel-native API
- Minimal configuration required
- Clean and testable

It does not aim to become a permissions framework, a full-disk encryption system, or a key management service.

Testing
-------

[](#testing)

```
composer test
```

Run static analysis:

```
composer analyse
```

Format code:

```
composer format
```

Contributing
------------

[](#contributing)

Contributions are welcome.

Please ensure:

- Tests pass (`composer test`)
- PHPStan passes (`composer analyse`)
- Code style is formatted with Pint (`composer format`)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review our [Security Policy](SECURITY.md) for reporting vulnerabilities.

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

Credits
-------

[](#credits)

Built and maintained by [Vimatech](https://vimatech.io). Created by [Adel Zemzemi](https://github.com/adelzemzemi).

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance92

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 92% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

3

Last Release

49d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3283664f06a0db1bfdbf282b2691363365c2f73569bcd99d63f5aaa52900ff55?d=identicon)[adelzemzemi](/maintainers/adelzemzemi)

---

Top Contributors

[![adelzemzemi](https://avatars.githubusercontent.com/u/272534830?v=4)](https://github.com/adelzemzemi "adelzemzemi (23 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (2 commits)")

---

Tags

laravelsecurityencryptioneloquentgdprpiiencrypted-fieldsaes-256-gcm

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/vimatech-io-laravel-secure-fields/health.svg)

```
[![Health](https://phpackages.com/badges/vimatech-io-laravel-secure-fields/health.svg)](https://phpackages.com/packages/vimatech-io-laravel-secure-fields)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M246](/packages/laravel-ai)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9762.4M133](/packages/roots-acorn)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[reedware/laravel-relation-joins

Adds the ability to join on a relationship by name.

2111.3M18](/packages/reedware-laravel-relation-joins)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5022.6k](/packages/simplestats-io-laravel-client)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
