developgravity / laravel-binary-encryption
Eloquent cast and encryption helper that gzip-compresses and encrypts into a compact versioned binary format.
Package info
github.com/DevelopGravity/Laravel-Binary-Encryption
pkg:composer/developgravity/laravel-binary-encryption
Fund package maintenance!
Requires
- php: ^8.5
- ext-zlib: *
- illuminate/contracts: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.29
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^4.6
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.1
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
This package is auto-updated.
Last update: 2026-04-20 00:35:09 UTC
README
An Eloquent cast that gzip-compresses and encrypts model attributes into a compact versioned binary format. All cryptography is delegated to Laravel's built-in Encrypter — this package only handles compression and binary packing for efficient storage.
Requirements
- PHP 8.5+
- Laravel 12 or 13
Installation
composer require developgravity/laravel-binary-encryption
The package auto-discovers its service provider and facade. No manual registration needed. It uses your application's
existing APP_KEY and app.cipher settings.
Usage
Apply the cast to any Eloquent model attribute:
use DevelopGravity\BinaryEncryption\Casts\BinaryEncryptedCast; class ApiCredential extends Model { protected function casts(): array { return [ 'secret_key' => BinaryEncryptedCast::class, 'webhook_secret' => BinaryEncryptedCast::class, ]; } }
Values are automatically encrypted on write and decrypted on read:
$credential = ApiCredential::create([ 'secret_key' => 'sk_live_abc123...', 'webhook_secret' => 'whsec_xyz789...', ]); // Stored as compact binary in the database // Transparently decrypted when accessed echo $credential->secret_key; // 'sk_live_abc123...'
Cast Subtypes
Like Laravel's built-in encrypted cast, you can encrypt structured data by appending a subtype:
use DevelopGravity\BinaryEncryption\Casts\BinaryEncryptedCast; class Integration extends Model { protected function casts(): array { return [ 'api_key' => BinaryEncryptedCast::class, // string 'settings' => BinaryEncryptedCast::class.':array', // array 'config' => BinaryEncryptedCast::class.':json', // array (alias) 'metadata' => BinaryEncryptedCast::class.':object', // stdClass 'tags' => BinaryEncryptedCast::class.':collection', // Collection ]; } }
| Subtype | get() returns |
set() accepts |
|---|---|---|
| (none) | string |
string |
:array |
array |
array or object |
:json |
array (alias for :array) |
array or object |
:object |
stdClass |
array or object |
:collection |
Illuminate\Support\Collection |
array or object |
Values are JSON-encoded before encryption and JSON-decoded after decryption.
Using the Facade Directly
use DevelopGravity\BinaryEncryption\Facades\BinaryEncrypt; $encrypted = BinaryEncrypt::encrypt('sensitive data'); $decrypted = BinaryEncrypt::decrypt($encrypted);
How It Works
- Compress — Plaintext is gzip-compressed to reduce storage size.
- Encrypt — The compressed data is encrypted using Laravel's Encrypter (
encryptString). - Repack — Laravel's base64+JSON output is decoded and repacked into a compact binary format with a versioned 3-byte header (version, cipher ID, flags), followed by the raw IV, authentication tag/MAC, and ciphertext with length prefixes.
- Decrypt reverses the process: unpack binary → reconstruct Laravel's expected payload format →
decryptString→ gzip decompress.
Supported Ciphers
| Cipher | ID |
|---|---|
| AES-256-CBC | 0x01 |
| AES-256-GCM | 0x02 |
| AES-128-CBC | 0x03 |
| AES-128-GCM | 0x04 |
The cipher is determined by your app.cipher config value.
Binary Format (v1)
[version: 1 byte] [cipher_id: 1 byte] [flags: 1 byte]
[iv_length: 2 bytes BE] [iv: variable]
[auth_length: 2 bytes BE] [auth: variable]
[ciphertext_length: 4 bytes BE] [ciphertext: variable]
Common Pitfalls
Null Bytes and PostgreSQL bytea Columns
The encrypted binary output frequently contains null bytes (\0). This is expected and correct — it's raw binary data,
not a text string.
The problem: PDO's default PDO::PARAM_STR binding truncates strings at the first null byte. If you store encrypted
values in a PostgreSQL bytea column using the default connection, your data will be silently truncated and
unrecoverable.
The solution: You need to ensure binary values containing null bytes are bound with PDO::PARAM_LOB instead of
PDO::PARAM_STR. Here's one approach using a custom connection:
// app/Database/PostgresConnection.php namespace App\Database; use Illuminate\Database\PostgresConnection as BasePostgresConnection; use PDO; class PostgresConnection extends BasePostgresConnection { public function prepareBindings(array $bindings): array { $grammar = $this->getQueryGrammar(); foreach ($bindings as $key => $value) { if ($value instanceof \DateTimeInterface) { $bindings[$key] = $value->format($grammar->getDateFormat()); } elseif (is_bool($value)) { $bindings[$key] = $value; } elseif (is_string($value) && str_contains($value, "\0")) { // Wrap binary strings for LOB binding $bindings[$key] = new BinaryValue($value); } } return $bindings; } public function bindValues($statement, $bindings): void { foreach ($bindings as $key => $value) { if ($value instanceof BinaryValue) { $statement->bindValue( is_string($key) ? $key : $key + 1, $value->bytes, PDO::PARAM_LOB, ); continue; } $statement->bindValue( is_string($key) ? $key : $key + 1, $value, match (true) { is_int($value) => PDO::PARAM_INT, is_bool($value) => PDO::PARAM_BOOL, is_resource($value) => PDO::PARAM_LOB, default => PDO::PARAM_STR, }, ); } } }
// app/Database/BinaryValue.php namespace App\Database; use Stringable; readonly class BinaryValue implements Stringable { public function __construct(public string $bytes) {} public function __toString(): string { return $this->bytes; } }
Register it in a service provider:
use Illuminate\Database\Connection; use App\Database\PostgresConnection; Connection::resolverFor('pgsql', function ($pdo, $database, $prefix, $config) { return new PostgresConnection($pdo, $database, $prefix, $config); });
Note: If you use a package that already provides a custom
pgsqlconnection resolver (e.g.tpetry/laravel-postgresql-enhanced), you'll need to extend that connection class instead of Laravel's base, since only one resolver can be active at a time.
MySQL and SQLite
MySQL BLOB/VARBINARY and SQLite BLOB columns handle null bytes correctly with PDO::PARAM_STR. No custom
connection is needed for these databases.
Testing
composer test
Or run Pest directly:
vendor/bin/pest
License
MIT License. See LICENSE.md for details.