Skip to content

Commit ac97e09

Browse files
Merge pull request #1 from ginkelsoft-development/develop
Develop
2 parents 87eec60 + fffe987 commit ac97e09

File tree

3 files changed

+134
-52
lines changed

3 files changed

+134
-52
lines changed

.github/workflows/tests.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
fail-fast: false
12+
matrix:
13+
include:
14+
# Laravel 8–10 werkt nog op PHP 8.1
15+
- php: 8.1
16+
laravel: 8.*
17+
- php: 8.1
18+
laravel: 9.*
19+
- php: 8.1
20+
laravel: 10.*
21+
22+
# Laravel 11 vereist minimaal PHP 8.2
23+
- php: 8.2
24+
laravel: 11.*
25+
26+
# Laravel 12 werkt optimaal met PHP 8.3
27+
- php: 8.3
28+
laravel: 12.*
29+
30+
name: PHP ${{ matrix.php }} / Laravel ${{ matrix.laravel }}
31+
32+
steps:
33+
- name: Checkout code
34+
uses: actions/checkout@v4
35+
36+
- name: Setup PHP
37+
uses: shivammathur/setup-php@v2
38+
with:
39+
php-version: ${{ matrix.php }}
40+
extensions: mbstring, pdo, sqlite, bcmath, intl
41+
coverage: none
42+
43+
- name: Cache Composer dependencies
44+
uses: actions/cache@v4
45+
with:
46+
path: vendor
47+
key: composer-${{ matrix.php }}-${{ matrix.laravel }}-${{ hashFiles('composer.lock') }}
48+
restore-keys: composer-
49+
50+
- name: Configure Laravel version
51+
run: composer require "illuminate/support:${{ matrix.laravel }}" "illuminate/database:${{ matrix.laravel }}" --no-update
52+
53+
- name: Install dependencies
54+
run: composer update --prefer-dist --no-interaction
55+
56+
- name: Run PHPUnit tests
57+
run: vendor/bin/phpunit --testdox --colors=always

README.md

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Ginkelsoft Laravel Encrypted Search Index
22

3+
[![Tests](https://github.com/ginkelsoft-development/laravel-encrypted-search-index/actions/workflows/tests.yml/badge.svg)](https://github.com/ginkelsoft-development/laravel-encrypted-search-index/actions/workflows/tests.yml)
4+
[![Latest Version on Packagist](https://img.shields.io/packagist/v/ginkelsoft/laravel-encrypted-search-index.svg?style=flat-square)](https://packagist.org/packages/ginkelsoft/laravel-encrypted-search-index)
5+
[![Total Downloads](https://img.shields.io/packagist/dt/ginkelsoft/laravel-encrypted-search-index.svg?style=flat-square)](https://packagist.org/packages/ginkelsoft/laravel-encrypted-search-index)
6+
[![License](https://img.shields.io/github/license/ginkelsoft-development/laravel-encrypted-search-index.svg?style=flat-square)](LICENSE.md)
7+
[![Laravel](https://img.shields.io/badge/Laravel-8--12-brightgreen?style=flat-square&logo=laravel)](https://laravel.com)
8+
[![PHP](https://img.shields.io/badge/PHP-8.1%20--%208.4-blue?style=flat-square&logo=php)](https://php.net)
9+
310
## Overview
411

512
Modern applications that handle sensitive user data—such as healthcare, financial, or membership systems—must ensure that all personally identifiable information (PII) is properly encrypted at rest. However, standard encryption creates a practical challenge: **once data is encrypted, it can no longer be searched efficiently.**
@@ -21,35 +28,35 @@ When data is fully encrypted, you lose the ability to perform meaningful queries
2128

2229
This package removes that trade-off by introducing a **detached searchable index** that maps encrypted records to deterministic tokens.
2330

24-
---\n
31+
---
2532

2633
## Key Features
2734

28-
* **Searchable encryption**: Enables exact and prefix-based searches over encrypted data.
29-
* **Detached search index**: Tokens are stored separately from the main data, reducing exposure risk.
30-
* **Deterministic hashing with peppering**: Each token is derived from normalized text combined with a secret pepper, preventing reverse-engineering.
31-
* **No blind indexes in primary tables**: Encrypted fields remain opaqueonly hashed references are stored elsewhere.
32-
* **High scalability**: Indexes can handle millions of records efficiently using native database indexes.
33-
* **Laravel-native integration**: Fully compatible with Eloquent models, query scopes, and events.
35+
* **Searchable encryption** Enables exact and prefix-based searches over encrypted data.
36+
* **Detached search index** Tokens are stored separately from the main data, reducing exposure risk.
37+
* **Deterministic hashing with peppering**Each token is derived from normalized text combined with a secret pepper.
38+
* **No blind indexes in primary tables**Encrypted fields remain opaque; only hashed references are stored elsewhere.
39+
* **High scalability** — Efficient for millions of records through database indexing.
40+
* **Laravel-native integration** — Works directly with Eloquent models, query scopes, and model events.
3441

3542
---
3643

3744
## How It Works
3845

39-
Each model can declare specific fields as searchable. When the model is saved, a background process normalizes the field value, generates one or more hashed tokens, and stores them in a separate database table named `encrypted_search_index`.
46+
Each model can declare specific fields as searchable. When the model is saved, the system normalizes the field value, generates one or more hashed tokens, and stores them in a separate table named `encrypted_search_index`.
4047

4148
When you search, the package hashes your input using the same process and retrieves matching model IDs from the index.
4249

4350
### 1. Token Generation
4451

4552
For each configured field:
4653

47-
* **Exact match token:** A SHA-256 hash of the normalized value plus a secret pepper.
48-
* **Prefix tokens:** Multiple SHA-256 hashes representing progressive prefixes of the normalized text (e.g., `w`, `wi`, `wie`).
54+
* **Exact match token:** A SHA-256 hash of the normalized value + secret pepper.
55+
* **Prefix tokens:** Multiple SHA-256 hashes representing progressive prefixes of the normalized text (e.g. `w`, `wi`, `wie`).
4956

5057
### 2. Token Storage
5158

52-
All tokens are stored in `encrypted_search_index` with the following structure:
59+
All tokens are stored in `encrypted_search_index`:
5360

5461
| model_type | model_id | field | type | token |
5562
| ----------------- | -------- | ---------- | ------ | ------ |
@@ -65,20 +72,20 @@ Client::encryptedExact('last_names', 'Vermeer')->get();
6572
Client::encryptedPrefix('first_names', 'Wie')->get();
6673
```
6774

68-
These queries use database-level indexes for efficient lookups even on large datasets.
75+
These use indexed lookups and remain performant even at scale.
6976

7077
---
7178

7279
## Security Model
7380

74-
| Threat | Mitigation |
75-
| ----------------------- | --------------------------------------------------------------------------- |
76-
| Database dump or breach | Tokens cannot be reversed to plaintext (salted and peppered SHA-256). |
77-
| Statistical analysis | Tokens are fully detached; frequency analysis yields no useful correlation. |
78-
| Insider access | No sensitive data in the index table; encrypted fields remain opaque. |
79-
| Leaked `APP_KEY` | Does not affect token security; the pepper is stored separately in `.env`. |
81+
| Threat | Mitigation |
82+
| ----------------------- | ----------------------------------------------------------------- |
83+
| Database dump or breach | Tokens cannot be reversed (salted + peppered SHA-256). |
84+
| Statistical analysis | Tokens are detached; frequency analysis yields no correlation. |
85+
| Insider access | No sensitive data in index table; encrypted fields remain opaque. |
86+
| Leaked `APP_KEY` | Irrelevant for tokens; pepper is stored separately in `.env`. |
8087

81-
The system follows a **defense-in-depth** approach: encrypted data remains fully protected, while token search provides limited, controlled visibility for queries.
88+
This design follows a **defense-in-depth** model: encrypted data stays secure, while search operations remain practical.
8289

8390
---
8491

@@ -90,7 +97,7 @@ php artisan vendor:publish --tag=config
9097
php artisan migrate
9198
```
9299

93-
Update your `.env` file with a unique pepper:
100+
Then add a unique pepper to your `.env` file:
94101

95102
```
96103
SEARCH_PEPPER=your-random-secret-string
@@ -100,7 +107,7 @@ SEARCH_PEPPER=your-random-secret-string
100107

101108
## Configuration
102109

103-
`config/encrypted-search.php`
110+
`config/encrypted-search.php`:
104111

105112
```php
106113
return [
@@ -131,47 +138,57 @@ class Client extends Model
131138
}
132139
```
133140

134-
When a `Client` record is saved, its searchable tokens are automatically created or updated in the `encrypted_search_index` table.
141+
When a record is saved, searchable tokens are automatically generated in `encrypted_search_index`.
135142

136143
### Searching
137144

138145
```php
139-
// Exact match search
146+
// Exact match
140147
$clients = Client::encryptedExact('last_names', 'Vermeer')->get();
141148

142-
// Prefix match search
149+
// Prefix match
143150
$clients = Client::encryptedPrefix('first_names', 'Wie')->get();
144151
```
145152

146153
### Rebuilding the Index
147154

148-
You can rebuild the entire search index using an Artisan command:
155+
Rebuild indexes via Artisan:
149156

150157
```bash
151158
php artisan encryption:index-rebuild "App\\Models\\Client"
152159
```
153160

154-
This will reprocess all searchable fields for the specified model.
155-
156161
---
157162

158163
## Scalability and Performance
159164

160-
* **Optimized database lookups**: The `encrypted_search_index` table uses compound indexes for fast token-based lookups.
161-
* **Chunked rebuilds**: The `index-rebuild` command supports chunked processing to handle large datasets efficiently.
162-
* **Asynchronous rebuilds**: Can be safely run in queues or background jobs.
165+
* **Indexed database lookups** for efficient token search.
166+
* **Chunked rebuilds** for large datasets (`--chunk` option).
167+
* **Queue-compatible** for asynchronous index rebuilds.
163168

164-
Unlike in-memory search systems, this index-based approach scales linearly with the size of your dataset and can efficiently handle millions of records.
169+
The detached index structure scales linearly and supports millions of records efficiently.
165170

166171
---
167172

168-
## Compliance
173+
## Framework Compatibility
174+
175+
| Laravel Version | PHP Version(s) Supported |
176+
| --------------- | ------------------------ |
177+
| 8.x | 8.0 – 8.1 |
178+
| 9.x | 8.1 – 8.2 |
179+
| 10.x | 8.1 – 8.3 |
180+
| 11.x | 8.2 – 8.3 |
181+
| 12.x | 8.3+ |
169182

170-
This approach aligns with major privacy and compliance frameworks:
183+
The package is continuously tested across all supported combinations using GitHub Actions.
184+
185+
---
186+
187+
## Compliance
171188

172-
* GDPR: Minimal data exposure; encrypted and hashed data separation.
173-
* HIPAA: Ensures ePHI remains protected even in breach scenarios.
174-
* ISO 27001: Supports layered security controls for data confidentiality.
189+
* **GDPR** — Encrypted and hashed separation ensures minimal data exposure.
190+
* **HIPAA** — Meets encryption-at-rest requirements for ePHI.
191+
* **ISO 27001** — Aligns with confidentiality and cryptographic control standards.
175192

176193
---
177194

composer.json

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,41 @@
11
{
22
"name": "ginkelsoft/laravel-encrypted-search-index",
3-
"description": "Searchable indexes for encrypted model fields in Laravel.",
3+
"description": "Encrypted and privacy-preserving search indexing for Laravel models.",
44
"type": "library",
55
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Wietse van Ginkel",
9+
"email": "info@ginkelsoft.com"
10+
}
11+
],
12+
"require": {
13+
"php": "^7.4 || ^8.0 || ^8.1 || ^8.2 || ^8.3",
14+
"illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
15+
"illuminate/database": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0"
16+
},
17+
"require-dev": {
18+
"orchestra/testbench": "^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0",
19+
"phpunit/phpunit": "^9.6 || ^10.0 || ^11.0"
20+
},
621
"autoload": {
722
"psr-4": {
823
"Ginkelsoft\\EncryptedSearch\\": "src/"
924
}
1025
},
26+
"autoload-dev": {
27+
"psr-4": {
28+
"Ginkelsoft\\EncryptedSearch\\Tests\\": "tests/",
29+
"Tests\\": "tests/"
30+
}
31+
},
1132
"extra": {
1233
"laravel": {
1334
"providers": [
1435
"Ginkelsoft\\EncryptedSearch\\EncryptedSearchServiceProvider"
1536
]
1637
}
1738
},
18-
"require": {
19-
"php": ">=8.2",
20-
"illuminate/support": "^11.0|^12.0",
21-
"illuminate/database": "^11.0|^12.0"
22-
},
23-
"require-dev": {
24-
"orchestra/testbench": "^9.0",
25-
"phpunit/phpunit": "^10.5|^11.0"
26-
},
27-
"autoload-dev": {
28-
"psr-4": {
29-
"Ginkelsoft\\EncryptedSearch\\Tests\\": "tests/",
30-
"Tests\\": "tests/"
31-
}
32-
}
39+
"minimum-stability": "stable",
40+
"prefer-stable": true
3341
}

0 commit comments

Comments
 (0)