Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Crypto Bundle Laravel Package

gdbots/crypto-bundle

Symfony bundle that integrates Gdbots crypto services into your PHP application, providing configuration and dependency injection for encryption, signing, and key management. Simplifies wiring secure crypto components and related services.

View on GitHub
Deep Wiki
Context7

Getting Started

Be clear-eyed about what this is: gdbots/crypto-bundle is a Symfony bundle, not a Laravel package. It exists to register defuse/php-encryption as a DI service (Keyring, key-loading, usually a CryptoManager wrapper and a key-generation console command) inside a Symfony kernel. You cannot $this->app->register() it — Laravel has no Bundle, no ContainerBagInterface, no Extension.

The v5.0.0 release changed nothing about that story, and made it slightly worse. The only change is a platform bump: Symfony 8.x and PHP 8.5+. That is a Symfony-LTS-tracking cadence, not a Laravel one, and it means the package now demands a PHP version most Laravel apps haven't adopted. Treat the bump as confirmation of the porting advice below, not as a reason to reconsider.

So the honest "getting started" is a port, and it takes about an hour:

  1. Read the bundle before installing it. Clone the repo and open, in this order: src/DependencyInjection/*Extension.php (config shape — key paths, key ids, cipher settings), Resources/config/services.* (service ids and constructor args), the manager/wrapper class, then the README. That is the entire spec for how gdbots expects keys to be laid out on disk. You are copying conventions, not code.
  2. Install the actual dependency, not the bundle:
    composer require defuse/php-encryption
    
    As of v5.0.0 do not add gdbots/crypto-bundle to require-dev "just to keep the source around." Its constraints (php >=8.5, symfony/* ^8.0) are solved alongside your app's, and require-dev packages participate in that resolution. On a normal Laravel app you'll get a solver conflict at best and a forced PHP upgrade at worst. Clone the repo into a scratch directory or a separate reference project instead — you only need to read it.
  3. Bind the keyring once, in a service provider:
    // app/Providers/CryptoServiceProvider.php
    use Defuse\Crypto\Key;
    use Defuse\Crypto\Keyring;
    
    public function register(): void
    {
        $this->app->singleton(Keyring::class, function () {
            $ring = new Keyring();
            // first key added is the primary (used for encryption)
            $ring->addKey(Key::loadFromAsciiSafeString(trim(config('crypto.keys.primary'))));
            foreach (config('crypto.keys.previous', []) as $ascii) {
                $ring->addKey(Key::loadFromAsciiSafeString(trim($ascii)));
            }
            return $ring;
        });
    }
    
    Pin defuse/php-encryption to ^2.0 in composer.json — the bundle's major version and defuse's major version move independently, and the v5 bump says nothing about the crypto API you're actually calling.

First use case: you have a sibling Symfony/CLI service (or a legacy gdbots job) writing defuse-encrypted blobs into a shared table or queue, and a Laravel app has to read them. That — decryption interop — is the only scenario where this package's conventions are worth copying. If you just need to encrypt a column of your own, Laravel's Crypt already does it and you should stop reading here.

Implementation Patterns

Wrap the keyring in a single application service. Never let Crypto::encrypt() calls scatter across controllers. Add an envelope prefix so you can version and rotate:

final class Cipher
{
    public function __construct(private Keyring $keyring) {}

    public function encrypt(string $plaintext): string
    {
        return 'v1:'.base64_encode(Crypto::encrypt($plaintext, $this->keyring));
    }

    public function decrypt(string $payload): string
    {
        [$v, $body] = array_pad(explode(':', $payload, 2), 2, '');
        $raw = base64_decode($body, true);
        if ($raw === false) {
            throw new DecryptException('Malformed ciphertext envelope.');
        }
        return Crypto::decrypt($raw, $this->keyring);
    }
}

Eloquent cast for encrypted columns — encrypt on set, decrypt on get, and store the base64 envelope, never the raw binary:

public function set($model, $key, $value, $attributes)
{
    return [$key => $value === null ? null : app(Cipher::class)->encrypt((string) $value)];
}

Rotation. defuse's Keyring adds new keys at the front and tries every key on decrypt, but it gives you no way to ask "which key encrypted this?" So put the key id in the envelope yourself (v2:k3:...) if you want cheap lazy re-encryption on read. Otherwise, rotate by writing new data with the new primary key and running a batched chunkById job that decrypts and re-encrypts old rows.

Blind indexes for lookups. defuse is non-deterministic — the same plaintext yields different ciphertext every time, so you cannot WHERE email_encrypted = ?. If you need to search, store a separate hash_hmac('sha256', mb_strtolower(trim($v)), $indexKey) column and query that. Keep the index key distinct from the encryption key.

Migrating off Laravel Crypt. Dual-read during rollout: try Cipher::decrypt(), catch the exception, fall back to Crypt::decryptString() with the legacy APP_KEY, and re-encrypt on the next write. Only drop the fallback after a full backfill.

Where Symfony's convenience lives, replace it. The bundle's console command for generating keys becomes an Artisan command:

$this->line(Key::createNewRandomKey()->saveToAsciiSafeString());

Print it once to a secret manager, never to a file in the repo.

Gotchas and Tips

  • v5.0.0 has no runtime feature change — don't chase it. The release notes are a single platform bump (PHP 8.5, Symfony 8.x). There is no new crypto behaviour, no API change, nothing to adopt. If you were somehow running the bundle inside a Symfony service next to your Laravel app, upgrading is a Symfony/PHP upgrade project, not a crypto one.
  • The version churn is a compatibility hazard, not a signal. v5 pins to Symfony 8, meaning v4 pins to Symfony 7, v3 to 6, and so on. If you ever do add it as a dependency, pin the major explicitly; an unconstrained ^5 will drag PHP 8.5 into your build matrix the moment CI resolves fresh.
  • Binary in a text column is the #1 failure. Crypto::encrypt() returns raw bytes. Piping that into MySQL utf8mb4 or JSON silently corrupts it and you get WrongKeyOrModifiedCiphertextException later with no clue why. Always base64 or hex at the storage boundary — Crypto::binToBase64() exists for this.
  • Size your columns generously. Sodium XChaCha20-Poly1305 overhead is 1 byte version + 24 nonce + 16 tag = 41 bytes, then base64 adds ~33%. A 255-char plaintext becomes ~396 chars. Use TEXT/LONGTEXT, and never truncate the envelope.
  • Catch the exception class explicitly and treat it as tampering or corruption, not a 500. WrongKeyOrModifiedCiphertextException extends CryptoException — log with row id and table, return 422/null. Do not let it bubble into an error page that leaks the payload.
  • Never use encryptWithPassword() for application data. PBKDF2 is deliberately slow (fine once per login, disastrous per-row in a query loop) and a password is a single shared secret you can't rotate per key. Use Key/Keyring.
  • Keys are ASCII-safe strings, and one stray newline kills everything. trim() the env value (see the provider above — a trailing newline in a .env heredoc is the classic offender), or better, load the ASCII string from a mounted secret file with 0600 permissions outside the webroot and chmod-check at boot. Fail fast in the provider via Key::loadFromAsciiSafeString() — a malformed key throws CryptoException at container resolution, which is exactly when you want to find out.
  • Keyring size is a decrypt cost. Decryption tries keys in order until one works. Cap previous keys at a handful and retire them with a backfill job, otherwise every read of an old row walks the whole ring.
  • Laravel Octane / queue workers: the singleton keyring is cached per worker for the process lifetime — good — but that means a rotated key requires a worker restart. Document that in your deploy runbook.
  • Don't encrypt what you JOIN on. Primary keys, foreign keys, tenant ids, and timestamps you filter by stay plaintext; encrypt only the sensitive payload columns.
  • Test by decrypting, not by asserting. Ciphertext is non-deterministic. assertSame($plain, $cipher->decrypt($cipher->encrypt($plain))), and write one test that decrypts a fixture blob produced by the real Symfony service — that is your interop regression test. Check that fixture in; it's the only thing that proves the two sides still agree after either of you upgrades.
  • Extension points are thin, and that's the point. The bundle is ~a handful of classes, so treat it as documentation of gdbots' key conventions rather than a dependency you inherit. Apache-2.0 means you can lift the config shape wholesale with attribution. Budget an afternoon to read the source; there is no meaningful docs site, and a major-version bump that touches only composer.json should tell you how much of your attention the project commands.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment