Versleutelde gegevens doorzoeken in Laravel zonder de beveiliging te slopen
Zodra je gevoelige gegevens versleutelt, kun je er niet meer fatsoenlijk op zoeken. Dit package lost dat op met een losse, gehashte zoekindex, zodat je data versleuteld blijft én vindbaar is. Ontstaan uit een echt probleem in een zorgdossier.
Installation
composer require ginkelsoft-development/laravel-encrypted-search-indexHet probleem: versleutelde data is veilig, maar onvindbaar
Stel, je bouwt een applicatie die met gevoelige persoonsgegevens werkt. Een zorgdossier, een ledenadministratie, een financieel systeem. De AVG en NEN 7510 verwachten dat je die gegevens versleuteld opslaat, en terecht. Laravel maakt dat eenvoudig met de ingebouwde encryptie en de encrypted cast.
Maar dan loop je tegen een muur aan. Zodra een veld versleuteld in de database staat, kun je er niet meer op zoeken. Een simpele where('last_names', 'Vermeer') levert niets op, want in de database staat geen Vermeer maar een onleesbare brij. Je hebt je gegevens netjes beveiligd en daarmee meteen onbruikbaar gemaakt voor het zoeken.
Dit is een probleem dat ik zelf tegenkwam bij het bouwen van een elektronisch cliëntdossier voor de zorg. De namen van cliënten moesten versleuteld, dat stond niet ter discussie. Maar een zorgmedewerker moet wel gewoon op een achternaam kunnen zoeken. Die twee eisen lijken elkaar uit te sluiten.
De gangbare oplossing lekt informatie
De meeste developers lossen dit op met een van twee compromissen, en geen van beide is fijn.
De eerste is alles versleutelen met een willekeurige sleutel per waarde. Maximale veiligheid, maar zoeken wordt volstrekt onmogelijk. De tweede is een deel van de gegevens als platte tekst of als een eenvoudige hash opslaan, zodat je kunt vergelijken. Dat maakt zoeken weer mogelijk, maar zo'n blind index lekt statistische patronen. Komt een achternaam vaak voor, dan is dat zichtbaar in de hashes, en dat opent de deur naar correlatie-aanvallen.
Je kiest dus tussen sterke beveiliging zonder zoeken, of zwakke beveiliging met zoeken. Dat is een rotkeuze, en in een gereguleerde omgeving wil je hem niet maken.
Hoe dit package het oplost
Het Laravel Encrypted Search Index package haalt die keuze weg. Het idee is een losse zoekindex, gescheiden van je eigenlijke gegevens. Je versleutelde velden blijven volledig versleuteld en onleesbaar. Daarnaast houdt het package in een aparte tabel gehashte tokens bij waarmee je kunt zoeken.
Wanneer je een record opslaat, normaliseert het package de waarde van een doorzoekbaar veld en maakt er een of meer tokens van. Een token is een SHA-256 hash van de genormaliseerde waarde, gecombineerd met een geheime pepper en een context per model en veld. Zoek je later, dan hasht het package je zoekterm op dezelfde manier en zoekt het de bijbehorende records op via die index.
Het belangrijke detail zit in die context per model en veld. Dezelfde achternaam in User.email en in Client.email levert verschillende tokens op. Daardoor kun je niet zien dat het om dezelfde waarde gaat, en sluit je correlatie tussen modellen uit. De pepper staat los in je .env, niet in de database, dus een datadump alleen levert een aanvaller niets bruikbaars op.
Een voorbeeld
Je markeert per model welke velden doorzoekbaar moeten zijn, met een attribuut:
php
use Ginkelsoft\EncryptedSearch\Attributes\EncryptedSearch;
class Client extends Model
{
#[EncryptedSearch(exact: true, prefix: true)]
public string $last_names;
}Daarna zoek je met twee scopes die het package aan je model toevoegt, een voor een exacte match en een voor een zoekopdracht op het begin van een woord:
php
// Exacte match
$clients = Client::encryptedExact('last_names', 'Vermeer')->get();
// Match op het begin (bijvoorbeeld voor autocomplete)
$clients = Client::encryptedPrefix('first_names', 'Wie')->get();Wil je over meerdere velden tegelijk zoeken, bijvoorbeeld omdat een naam in het voornaam- of het achternaamveld kan staan, dan is daar een variant voor met OR-logica die dubbele resultaten automatisch wegfiltert.
Installatie
bash
composer require ginkelsoft/laravel-encrypted-search-index php artisan vendor:publish --provider="Ginkelsoft\EncryptedSearch\EncryptedSearchServiceProvider" --tag=config php artisan vendor:publish --provider="Ginkelsoft\EncryptedSearch\EncryptedSearchServiceProvider" --tag=migrations php artisan migrate
Voeg daarna een unieke pepper toe aan je .env. Dit is een geheime, willekeurige tekenreeks die de tokens onomkeerbaar maakt. Bewaar hem zorgvuldig, want als je hem later wijzigt kloppen je bestaande tokens niet meer.
SEARCH_PEPPER=een-willekeurige-geheime-string
Voor grote datasets en hoge eisen
Standaard slaat het package de tokens op in een databasetabel, en dat schaalt prima tot in de miljoenen records dankzij reguliere database-indexen. Heb je meer nodig, dan kun je Elasticsearch als zoekmotor inschakelen. Je applicatiecode blijft dan exact hetzelfde, want dezelfde zoekscopes werken op beide backends. Alleen de opslag eronder verandert. Voor het opnieuw opbouwen van de index is er een Artisan-command die in stukken werkt en met een queue overweg kan, zodat je ook bij grote hoeveelheden geen geheugenproblemen krijgt.
Wanneer is dit geschikt, en wanneer niet
Dit package is bedoeld voor exacte zoekopdrachten en zoeken op het begin van een woord, bijvoorbeeld op namen, e-mailadressen of dossiernummers. Daar is het sterk in, en het doet dat zonder je beveiliging te ondermijnen.
Wat het niet doet, is zoeken op een woord midden in een tekst of vrije zoekopdrachten door grote lappen versleutelde tekst. Dat kan ook niet, want dat zou precies de patronen blootleggen die je juist wilt verbergen. Zoek je dat soort volledige tekstzoekfunctie over versleutelde documenten, dan is dit niet het juiste gereedschap.
Voor het doorzoekbaar maken van versleutelde persoonsgegevens in een Laravel-applicatie, met de AVG, HIPAA of ISO 27001 in het achterhoofd, is het dat wel. Het is open source, het draait in productie, en het is ontstaan uit een probleem dat ik in mijn eigen werk moest oplossen.