Decrypt to recover the plaintext. Security comes from controlling which actions are permitted to use the vault PKP — the immutable IPFS CID lets you (and anyone else) audit that the permitted code doesn’t return or log the plaintext.
The model
The trust anchor is the on-chain configuration that says “this IPFS CID may use this PKP.” Anyone who can’t get the gateway to run that exact code against that exact PKP cannot decrypt the ciphertext, no matter what else they hold. A permitted action can exfiltrate the plaintext if its code chooses to — so what matters is that you only permit code you’ve audited.
Lifecycle: encrypt once, decrypt at runtime
1. Mint a vault PKP
Create a PKP for your secrets through the Dashboard or viacreateWallet. Give it a name that describes the data boundary it protects — e.g. app-secrets, user-alice-vault, oracle-api-keys.
One PKP per logical data boundary is the recommended pattern. See PKP Wallets as Data Vaults for the rationale.
2. Permit your actions to use the PKP via a group
Permissioning happens through groups: a group binds a set of PKPs, a set of permitted IPFS action CIDs, and the usage API keys that can run them together. An action can only callEncrypt or Decrypt against a PKP when both the action’s IPFS CID and the PKP are in the same group.
You need to add to the group:
- The vault PKP from step 1.
- The IPFS CID of the encrypt action you’ll run in step 3.
- The IPFS CID of every production action that will decrypt the secret at runtime (step 4).
- The usage API key that will trigger these actions.
AccountConfig contract. See the Groups guide for the full model.
If you publish a new version of your decrypt action, its IPFS CID changes — you’ll need to add the new CID to the group (and remove the old one if you want to retire it).
3. Encrypt the secret once
Run an action that returns the ciphertext. You only run this when the secret changes.ciphertext is opaque without the vault PKP. Store it wherever fits your app:
- Bake it into your production action source code (ciphertext becomes part of the immutable IPFS CID)
- Pass it via
js_paramsfrom your backend - Store it in a database or on-chain registry alongside metadata
4. Decrypt at runtime in your production action
Your production action receives the ciphertext, decrypts, uses the plaintext, and returns only the result — never the secret itself.Where to put the ciphertext
The ciphertext is safe in the open. Pick the storage option that matches how the secret is consumed:
In all cases, callers can hold and pass the ciphertext freely — the gating point is whether they can get your action to run against the vault PKP.
Multiple secrets
You have two natural shapes. Pick the one that matches how the secrets are used together, not how they’re stored. One secret per ciphertext. Encrypt each secret as a separate call. Pass only the ones you need.client_id + client_secret pair, or an API key + endpoint URL).
Rotating a secret
A ciphertext is bound to the vault PKP, not to the secret value. Rotating means re-encrypting the new secret against the same PKP and replacing the old ciphertext wherever it’s stored.Securing an RPC URL with an embedded API key
A common case worth calling out: many RPC providers require the API key in the URL itself (e.g.https://mainnet.infura.io/v3/YOUR_KEY). Passing the full URL through js_params would leak the key. Instead, hardcode the base URL in the action (so observers can verify the target chain) and decrypt just the key at runtime.
See Securing RPC URLs for the full pattern.
Common mistakes
- Returning the plaintext secret in the action response. Whatever you
returnfrommainreaches the caller. Decrypt, use, and return only the result of using the secret. - Logging the plaintext via
console.log. Lit Action logs are visible to the caller. - Passing the unencrypted secret via
js_params.js_paramsare caller-supplied and visible in the request — exactly the wrong place for a secret. Only ciphertexts and identifiers belong there. - Sharing one vault PKP across unrelated apps. If an attacker convinces the on-chain config to permit a malicious action against the vault PKP, every secret in that vault is exposed. One PKP per concern keeps blast radius small.
- Forgetting that ciphertext baked into action source is immutable. Once the action is published to IPFS, you can’t edit the embedded ciphertext — rotate by publishing a new action.
What’s enforced where
The first three are network-level guarantees. The fourth — auditing what the permitted code actually does with the plaintext, and who is allowed to trigger a decrypt — is your responsibility. See Gating Logic for patterns.
See also
- Encrypt / Decrypt — PKP Wallets as Data Vaults — the broader vault pattern, including gated decrypt actions for dApp users.
- Multiple PKPs in a Single Action — use one PKP as a shared secrets vault while giving each end user a discrete PKP for signing, all in one execution.
- Securing RPC URLs — the canonical embedded-API-key walkthrough.
- Lit Actions SDK reference —
Encrypt/DecryptAPI. - Encryption migration notes — how Chipotle’s TEE-derived encryption differs from the older BLS threshold model.