tourze/json-rpc-encrypt-bundle
A Symfony bundle that adds payload encryption to JSON-RPC services, helping secure request and response data exchanged between clients and servers. It integrates with the framework's configuration and dependency injection, making encrypted RPC endpoints easier to set up and maintain in PHP applicati...
Install and confirm what you actually got. The package is published as a bundle (json-rpc-encrypt-bundle), which is Symfony terminology. Before writing any integration code, verify whether 2.0.0 ships a Laravel service provider or only a Symfony Bundle class:
composer require tourze/json-rpc-encrypt-bundle:^2.0
# What did I actually install?
cat vendor/tourze/json-rpc-encrypt-bundle/composer.json | jq '.require, .autoload'
grep -rl "Illuminate\\\\Support\\\\ServiceProvider" vendor/tourze/json-rpc-encrypt-bundle/src || echo "No Laravel provider"
ls vendor/tourze/json-rpc-encrypt-bundle/src vendor/tourze/json-rpc-encrypt-bundle/config 2>/dev/null
Where to look first, in this order:
composer.json — PHP constraint, symfony/* vs illuminate/* dependencies, and whether extra.laravel.providers exists. A 2.0.0 major bump almost certainly moved the minimum PHP/Symfony version.config/ — the shipped default configuration keys. This is the fastest map of the package's surface area.tests/ — with an empty changelog, the test suite is your only reliable usage documentation. Read the fixture/factory setup before the production classes.src/ for the encryptor contract and the payload envelope (key id, nonce, cipher, MAC fields).First use case to get working: one round trip, both directions, in a feature test — encrypt a JSON-RPC request, decrypt it back, and assert the envelope shape. Do not start with the HTTP middleware; prove the crypto layer in isolation first, because encryption failures surface as opaque "MAC invalid" / "bad decrypt" errors that are hard to attribute once a middleware stack is involved.
// tests/Feature/EncryptRoundTripTest.php
public function test_envelope_survives_a_round_trip(): void
{
$encryptor = app(YourEncryptorContract::class);
$payload = ['jsonrpc' => '2.0', 'method' => 'ping', 'params' => [], 'id' => 1];
$wire = $encryptor->encrypt(json_encode($payload));
$this->assertSame($payload, json_decode($encryptor->decrypt($wire), true));
}
Bind the encryptor as a singleton, configured from env, never from committed config. Keys belong in .env and must be decoded deliberately — most "bad decrypt" bugs are a base64 key being read as a raw string.
// app/Providers/JsonRpcEncryptServiceProvider.php
public function register(): void
{
$this->app->singleton(YourEncryptorContract::class, fn ($app) => new YourEncryptor(
key: base64_decode(config('services.jsonrpc.key')),
keyId: config('services.jsonrpc.key_id'),
cipher: config('services.jsonrpc.cipher', 'aes-256-gcm'),
));
}
public function boot(): void
{
if (is_dir($path = dirname(__DIR__, 2).'/vendor/tourze/json-rpc-encrypt-bundle/config')) {
$this->mergeConfigFrom($path.'/whatever_the_file_is.php', 'jsonrpc-encrypt');
$this->publishes([$path => config_path('jsonrpc-encrypt.php')], 'jsonrpc-encrypt-config');
}
}
Inbound: decrypt in middleware, then replace the request body. Decrypting without rewriting the request leaves $request->all() empty downstream, which is the single most common integration failure.
public function handle(Request $request, Closure $next): Response
{
$decrypted = $this->encryptor->decrypt($request->getContent());
$request->replace(json_decode($decrypted, true, flags: JSON_THROW_ON_ERROR));
$response = $next($request);
return $response->setContent(
$this->encryptor->encrypt($response->getContent())
);
}
Encrypt error envelopes too. A JSON-RPC client that must decrypt the result body but receives a plaintext -32600 error cannot parse it. Register an exception handler mapping for the RPC endpoints that renders through the same encryptor, or make the middleware catch throwables and produce an encrypted {"jsonrpc":"2.0","error":{...},"id":null}.
Outbound: wrap, don't fork, the HTTP client. Keep transport concerns (timeout, retry, base URL) in Laravel's Http client and only transform the body.
Http::baseUrl(config('services.rpc.url'))
->withBody($this->encryptor->encrypt(json_encode($envelope)), 'application/json')
->post('/rpc')
->throw()
->body();
// then decrypt + json_decode, and surface JSON-RPC `error` as an exception
Version the key id in the envelope and keep two keys live. Rotation then becomes a config change plus a background sweep, not a coordinated deploy. Decrypt by looking up keyId in a small keyring; always encrypt with the current key.
Testing: fake the crypto, assert on plaintext. Bind a passthrough encryptor in TestCase::setUp() so feature tests read as ordinary JSON-RPC tests, then keep a small number of real-crypto tests for the encryptor itself. Assert on decrypted payloads, never on ciphertext literals.
2.0.0 has no published details — treat it as breaking until proven otherwise. The release notes are a bare compare link (1.0.1...2.0.0). Before upgrading, read the diff and specifically diff composer.json:
git diff 1.0.1 2.0.0 -- composer.json
git diff 1.0.1 2.0.0 -- src/ | grep -E '^[-+].*(class|interface|function|const)'
Then run your suite with composer why-not to catch conflicting transitive constraints. A major bump from a tourze/* package typically means namespace moves, constructor signature changes, or a dropped PHP/Symfony floor — none of which are forward-compatible.
"Bundle" means no Laravel auto-discovery until you verify otherwise. If extra.laravel.providers is absent and no ServiceProvider exists in src/, you own the wiring. Writing a thin 30-line provider is usually cheaper than waiting for upstream support, but pin the version tightly (^2.0.0 with a lockfile commit) because 2.x could rename the very classes you wrapped.
Double-encoding of ciphertext. Ciphertext is binary; it must be base64 or hex encoded before entering JSON. If a framework layer already encodes the body, you get a payload the server decrypts into base64 text. Symptom: decryption "succeeds" but json_decode returns null. Assert json_last_error() in tests.
Encryption is not integrity of intent. Encrypting the body stops eavesdropping but does nothing about replay. Add a nonce and a signed timestamp inside the envelope and reject stale requests; otherwise a captured transfer call is replayable forever. Also consider signing method+params rather than only encrypting them if the key is shared across clients.
Cache pollution. Encrypted responses are per-client-secret and must not land in a shared cache. Ensure Cache-Control: no-store / correct Vary headers, and never cache a response produced by the middleware above.
Streaming and large payloads. Whole-envelope encryption forces full buffering in memory. For anything with large result/params (file chunks, paginated data), encrypt only the params field or use a streaming AEAD — decide before you build the middleware, since retrofitting changes the wire format.
Debugging without leaking. Gate decrypted-body logging behind app()->isLocal() and a separate flag; a decrypt log line in production is a plaintext secret in a log aggregator. For the "MAC invalid" class of error, check in this order: key encoding (base64 vs raw), cipher string spelling, IV/nonce length, then envelope field order for MAC computation.
Extension points worth checking before you write glue code: a serializer/encryptor interface you can implement (useful for swapping in libsodium or KMS-backed keys), and any Symfony event dispatcher hooks — in Laravel those need an adapter or you skip them entirely and call the service directly.
Maintenance risk. One-person tourze/* ecosystems are common and often healthy, but verify tag cadence and that 1.0.1 → 2.0.0 isn't a lone commit. If you adopt this for anything security-bearing, pin to an exact version and keep the encryption behind your own interface so the package is replaceable in an afternoon.
How can I help you explore Laravel packages today?