manojx/encrypter-bundle
manojx/encrypter-bundle is a lightweight encryption wrapper for Laravel and PHP applications, bundling encrypt/decrypt helpers and a simple service for protecting sensitive strings consistently across projects.
First, the reality check. manojx/encrypter-bundle is a Symfony bundle, not a Laravel package. It ships a *Bundle class intended for config/bundles.php, has no composer.json extra.laravel block, no service provider, no facade, and nothing for Laravel's package discovery to find. composer require manojx/encrypter-bundle will install it, and Laravel will simply ignore it. Before you write any code, ask whether you actually need it.
The honest first step for a Laravel developer:
config/app.php and confirm key is set (it is, unless you skipped php artisan key:generate).use Illuminate\Support\Facades\Crypt;
$cipher = Crypt::encryptString('sk_live_abc123'); // base64 payload, AES-256-CBC + HMAC
$plain = Crypt::decryptString($cipher);
That is AES-256-CBC, authenticated, key-rotated via APP_KEY, with zero new dependencies. For 95% of "I need to encrypt a column / token / PII field" tasks in Laravel, this package adds risk and nothing else.
When you do need it: if you're integrating with an existing Symfony service (or a legacy PHP codebase) that already produces ciphertext with this bundle, and you must decrypt it inside Laravel. In that case:
composer require manojx/encrypter-bundlevendor/manojx/encrypter-bundle/src/ — the repo is small and undocumented; the encrypter class is the only thing you care about. Note its constructor signature (key, cipher, IV handling) and whether it is final.First use case to validate: take one real ciphertext string produced by the Symfony side, decrypt it in a tinker session, and confirm you get the expected plaintext. Do this before designing anything around it — the IV and key-encoding conventions are where these integrations die.
Bind it once, behind your own interface. Don't let the bundle's class leak into controllers and models. Define App\Contracts\Encrypter with encrypt(string): string / decrypt(string): string, and implement it by delegating to either Laravel's Crypt or the bundle's encrypter. This is the single highest-value pattern here: it makes the bundle a swappable detail instead of a load-bearing dependency.
// app/Providers/EncryptionServiceProvider.php
$this->app->singleton(Encrypter::class, fn () => new BundleEncrypter(
key: base64_decode(config('encryption.legacy_key')),
cipher: 'aes-256-cbc',
));
Eloquent cast for encrypted columns. The natural Laravel idiom is a custom cast, not manual encrypt() calls scattered around:
class EncryptedCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes) {
return $value === null ? null : app(Encrypter::class)->decrypt($value);
}
public function set($model, $key, $value, $attributes) {
return [$key => $value === null ? null : app(Encrypter::class)->encrypt($value)];
}
}
Bind it on the model (protected $casts = ['api_secret' => EncryptedCast::class]). Now every read/write path is covered, including toArray(), serialization, and firstOrCreate.
Read-through migration, not a big-bang rewrite. If you're moving off this bundle onto Laravel's Crypt, keep both keys available and decrypt with a fallback, then re-encrypt lazily on write:
public function decrypt(string $value): string
{
try {
return Crypt::decryptString($value); // new format
} catch (DecryptException) {
return $this->legacy->decrypt($value); // bundle format
}
}
Pair this with a php artisan command that iterates rows, decrypts via the fallback, and re-encrypts with Crypt. Run it, then delete the legacy branch.
Cross-boundary payloads. Encrypt before pushing to a queue or an external API only when the payload itself is sensitive (tokens, PII). For queue jobs, Laravel already encrypts the payload if you use ShouldBeEncrypted on the job — use that rather than hand-rolling.
Interop with a Symfony service. Align explicitly on four things and write them down: cipher (aes-256-cbc), key material (raw vs base64 — Laravel's APP_KEY is base64 with an base64: prefix; this bundle likely expects raw bytes), IV placement (prepended to ciphertext, or separate field?), and output encoding (raw base64 vs JSON envelope). If the bundle returns bare base64 ciphertext with an embedded IV, it is not wire-compatible with Illuminate\Encryption\Encrypter, which emits a base64-encoded JSON object with iv, value, mac, and tag. Do not assume Crypt::decryptString($symfonyCiphertext) will work.
No authentication tag — this is the big one. AES-256-CBC is malleable. Laravel's encrypter appends an HMAC-SHA256 and throws DecryptException on tampering. A minimal CBC-only encrypter typically does not. If this bundle gives you raw CBC, your ciphertext is forgeable by anyone who can write to the database. Mitigate by wrapping it: store hmac_sha256(ciphertext, $macKey) alongside, verify with hash_equals() before decrypting. Or sidestep the whole issue and use AES-256-GCM.
IV handling. CBC requires a fresh random IV per message. Verify in src/ that the IV is generated with random_bytes(openssl_cipher_iv_length(...)) and stored per-message. A hardcoded or key-derived IV is a critical defect — walk away if you find one.
Key encoding mismatch. base64_decode() before passing to the bundle if it expects 32 raw bytes. A 16-byte or truncated key silently produces garbage or an OpenSSL warning depending on OPENSSL_* flags — always check the return value of openssl_encrypt()/openssl_decrypt(); they return false on failure rather than throwing.
config:cache will bite you. Read the key via config('...'), never env() at runtime in a service provider closure — after php artisan config:cache (standard in production deploys), env() returns null and your key silently becomes empty.
Licensing and trust. The license is NOASSERTION — that is not a permissive license, it's an inability to identify one. Get a human to check LICENSE in the repo before this touches production. Combined with 0 stars, 0 dependents, and a single release in Oct 2024, treat the crypto as unaudited: read the src/ yourself, it's small. For anything regulated (PCI, HIPAA, GDPR-sensitive), prefer Crypt or sodium_crypto_secretbox.
Logging leaks. DecryptException messages and stack traces can include ciphertext; dd() in a controller dumps plaintext into logs. Wrap decrypts in try/catch, log a generic message with a correlation ID, and never log the plaintext or the key.
Testing. Add a round-trip test and a tamper test (flip a byte, assert it throws). The tamper test is the one that tells you whether authentication exists — if it doesn't throw, you've confirmed the gap above.
Extension point. Because the bundle exposes no Laravel integration, your service provider binding is the extension point. That's a feature: swapping to Crypt, sodium, or a KMS-backed encrypter later is a one-line container change, provided nothing type-hints the bundle's concrete class.
How can I help you explore Laravel packages today?