Skip to content

About

CFPayloadShield is Cloudflare Wrangler that protects your payload by encryption/decryption

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

CFPayloadShield

CFPayloadShield is a Cloudflare Wrangler Worker that decrypts PayloadShield requests before proxying them to your origin and encrypts upstream responses before returning them to clients. Its standalone JavaScript crypto module uses the Web Crypto API and can also be used in supported npm frameworks.

Protocol compatibility

The Worker uses the existing PayloadShield envelope:

{"encrypted":"<standard Base64 ciphertext>"}
  • aes-gcm-256: Base64 of 12-byte nonce || ciphertext || 16-byte tag.
  • rsa-hybrid: Base64 of a JSON bundle containing Base64 key, nonce, and data. key is a random 32-byte AES key wrapped with RSA-OAEP/SHA-256; data is AES-256-GCM ciphertext followed by its tag.

The plaintext is bytes supplied by the caller; for JSON APIs, serialize and parse JSON in your application. The Worker supports aes-gcm-256 and rsa-hybrid. It rejects chacha20-poly1305 because Cloudflare's Web Crypto API does not implement that cipher. Crypto failures never fall back to plaintext.

Install and run

Requires Node.js 22+ and npm for the current Wrangler v4 CLI. The standalone crypto package itself supports Node.js 20+ and other runtimes with Web Crypto.

npm install
npm test

Set the upstream origin and AES key as Wrangler secrets. The key must be the standard Base64 encoding of exactly 32 cryptographically random bytes:

npx wrangler secret put ORIGIN_URL
npx wrangler secret put PAYLOADSHIELD_KEY_B64
npx wrangler dev

Generate a key value locally with PowerShell, then paste the output at the secret prompt:

[Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32))

To run the real local round-trip smoke test in VS Code's integrated terminal, open three terminal tabs in the project folder. First create .dev.vars with a temporary AES key; this file is ignored by git:

$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
$bytes = New-Object byte[] 32
$rng.GetBytes($bytes)
$rng.Dispose()
$key = [Convert]::ToBase64String($bytes)
@("ORIGIN_URL=http://127.0.0.1:8091", "PAYLOADSHIELD_KEY_B64=$key") |
  Set-Content .dev.vars -Encoding ascii

Then run each command in its own terminal tab, leaving the first two running:

npm run test:origin
npm run dev
npm run test:local

The local smoke test posts an encrypted sample payload to Wrangler, checks that the mock origin received plaintext, and decrypts the encrypted response. It reads the test key from .dev.vars automatically. Stop the origin and Wrangler with Ctrl+C when finished, then remove the local key file with Remove-Item .dev.vars. Wrangler v4 requires Node.js 22+; check with node -v if npm run dev reports a runtime-version error. Set PAYLOADSHIELD_LOCAL_URL if Wrangler is listening on a different address.

wrangler.toml selects the Worker entry point, AES-GCM algorithm, and default 10 MiB plaintext limit. The Worker appends the incoming path and query string to ORIGIN_URL. Use an HTTPS origin for production.

To deploy:

npx wrangler deploy

For RSA-Hybrid, change PAYLOADSHIELD_ALGORITHM in wrangler.toml to rsa-hybrid, then add the role-specific keys as Wrangler secrets:

npx wrangler secret put PAYLOADSHIELD_PRIVATE_KEY_PEM
npx wrangler secret put PAYLOADSHIELD_PUBLIC_KEY_PEM

The private key must be unencrypted PKCS#8 PEM (BEGIN PRIVATE KEY) and decrypt client requests. The public key must be SubjectPublicKeyInfo PEM (BEGIN PUBLIC KEY) and encrypt responses for the client. RSA keys must be at least 2048 bits. These keys have different roles and normally belong to different key pairs. Do not put secret key contents in wrangler.toml, source control, or logs.

Reusable crypto package

The workspace package at packages/payloadshield-crypto has no runtime dependencies and exports encryptBytes, decryptBytes, encryptPayload, decryptPayload, encodeBase64, and decodeBase64. Import it as @payloadshield/crypto from this npm workspace, or publish/install that package to use it in another project.

import { decryptPayload, encryptPayload } from "@payloadshield/crypto";

const key = new Uint8Array(32); // Replace with a securely generated, shared key.
const config = { algorithm: "aes-gcm-256", key };
const plaintext = new TextEncoder().encode(JSON.stringify({ message: "hello" }));
const envelope = await encryptPayload(plaintext, config);
const recovered = await decryptPayload(envelope, config);

The functions accept bytes so the package does not assume a framework or serialization format. For RSA-Hybrid, use a Web Crypto CryptoKey or provide a PKCS#8 private PEM for decryptPayload and an SPKI public PEM for encryptPayload. Set maxPayloadSize on the config to enforce an application specific plaintext limit.

Security and deployment notes

  • Payload protection does not replace HTTPS. Use TLS from clients to Cloudflare and from Cloudflare to the origin.
  • The Worker buffers each body in memory within the configured limit. Choose a limit suitable for Cloudflare's request limits and your response workloads.
  • Encrypted responses are marked Cache-Control: no-store to avoid caching ciphertext that is tied to a particular key or request.
  • The AES-GCM key is shared by both directions. RSA-Hybrid uses the server private key for request decryption and the configured client-recipient public key for response encryption.
  • This prototype does not add client identity, replay prevention, request signing, automatic key rotation, or per-client key selection. Configure origin access controls and rotate keys deliberately.
  • Configure a lower maximum where possible. PAYLOADSHIELD_MAX_BODY_SIZE is a positive number of plaintext bytes.

See Step.md for the implementation steps and the reason behind each major choice.

About

CFPayloadShield is Cloudflare Wrangler that protects your payload by encryption/decryption

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages