developgravity/laravel-binary-encryption

Eloquent cast and encryption helper that gzip-compresses and encrypts into a compact versioned binary format.

Maintainers

Package info

github.com/DevelopGravity/Laravel-Binary-Encryption

pkg:composer/developgravity/laravel-binary-encryption

Fund package maintenance!

DevelopGravity

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-develop 2026-04-19 20:18 UTC

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

  1. Compress — Plaintext is gzip-compressed to reduce storage size.
  2. Encrypt — The compressed data is encrypted using Laravel's Encrypter (encryptString).
  3. 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.
  4. 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 pgsql connection 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.