laravel-binary-encryption maintained by developgravity
Laravel Binary Encryption Cast
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.