anomaly/encrypted-field_type
Adds an encrypted field type to Anomaly/Streams, letting you store sensitive values in encrypted form. It integrates as a field type so encrypted data can be managed through your existing Streams forms and modules, with encryption handled by the package.
This is a PyroCMS/Streams Platform field type addon, not a standalone Laravel package — it only does anything once the anomaly/streams platform is installed in a Laravel app.
Minimal steps:
composer require anomaly/encrypted-field_type (Laravel 5.5+/Pyro 3.x auto-discovers Anomaly\EncryptedFieldType\EncryptedFieldTypeServiceProvider). On older setups add the provider to config/app.php.APP_KEY is set and treated as production infrastructure. The field type delegates to Laravel's Crypt facade; if the key changes, every stored value becomes undecryptable.php artisan vendor:publish --tag=encrypted_field_type.Where to look first in the repo:
src/EncryptedFieldType.php — the type definition (slug encrypted, column type, validation).src/EncryptedFieldTypePresenter.php — what $entry->field actually returns in views (plaintext vs. ciphertext).resources/config/config.php — any cipher/key overrides.src/EncryptedFieldTypeServiceProvider.php — bindings and route/asset registration.First use case: a per-tenant API token or TOTP secret on an existing stream. In the control panel: Streams → Fields → Create Field, type Encrypted, assign it to a stream; or in a migration/seed via the Streams API:
$this->fields->add([
'slug' => 'api_secret',
'type' => 'anomaly.field_type.encrypted',
'config' => ['input_type' => 'text'],
]);
Then $entry->api_secret = 'plaintext'; $entry->save(); and read it back with $entry->api_secret — encryption/decryption is transparent at the assignment boundary.
Assign to streams declaratively. Prefer defining the field in a stream migration/installer rather than clicking it in the CP, so staging and production agree. Keep the plaintext out of the migration — it should only define the column.
Read/write through the entry.
$entry->api_secret = $request->input('api_secret'); // encrypted on set
$entry->save();
$token = $entry->api_secret; // decrypted accessor
$token = $entry->api_secret->value(); // presenter form, if you need the raw string
Always check the presenter when wiring Blade views — some field types return a decorated object, and calling string functions on it fails.
Typical day-to-day targets: third-party API keys, webhook signing secrets, OAuth refresh tokens, SMTP passwords stored in a settings stream, per-tenant credentials in a multi-tenant Pyro install.
Form integration is boring on purpose. The field type renders a standard text/textarea input and runs the platform's usual validation. Use input_type => 'textarea' for long secrets (PEM keys, JWT signing material) so the ciphertext column doesn't get truncated.
Rotate keys with a migration command, not by hand.
foreach (TenantModel::cursor() as $tenant) {
$plain = $tenant->api_secret; // decrypt with current key
$tenant->api_secret = $plain; // re-encrypt, new key
$tenant->save();
}
Run it in batches, log the entry IDs you've processed, and keep the old key available until the batch verifies clean.
Extension point: subclass EncryptedFieldType and override the encrypt/decrypt helpers to route through KMS/Vault or to swap in encryptString/decryptString. Register your subclass in your own addon's service provider rather than forking.
Pair with a deterministic lookup column whenever you need to find rows by the encrypted value (see below). Store hash_hmac('sha256', $plain, config('app.key')) in a normal indexed column and query on that.
Crypt derives from APP_KEY. Use APP_PREVIOUS_KEYS (Laravel 11+) or override the encrypter binding, then re-encrypt rows. Back up the key alongside every database dump — a dump without its key is worthless here.where('api_secret', $value), ORDER BY, LIKE, and distinct-value counts are all impossible. This is the biggest architectural constraint; plan the hash-lookup column up front.DecryptException at read time, which is ugly to diagnose.where-based filtering on the field are effectively dead. Hide or exclude the field from searchable config in the stream definition.database/seeds, stream installer data, or the repo. Use .env/vault-backed values and a command to populate.DecryptException. Wrap reads in a try/catch in presenters and return null or a masked placeholder; a single bad row otherwise blows up an entire listing page.php artisan tinker → Crypt::decryptString(DB::table('...')->value('api_secret')) confirms storage; dump the raw column to verify it's actually ciphertext and not silently plaintext via a cached field type.APP_KEY in phpunit.xml. Tests that encrypt then compare bytes will fail; assert on decrypted values only.config('app.cipher')/config('app.key'); mixing AES-256-CBC and AES-256-GCM across environments breaks portability of dumps between environments even with the same key.composer.json, and if it disappears, the fallback is a five-line custom field type using Crypt::encryptString() on a TEXT column. Budget an afternoon for that migration rather than assuming upstream fixes arrive.How can I help you explore Laravel packages today?