> ## Documentation Index
> Fetch the complete documentation index at: https://developer.litprotocol.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Secrets

> How to use secrets — API keys, tokens, credentials — inside a Lit Action. The PKP-as-vault model, the encrypt-once / decrypt-at-runtime lifecycle, where to store ciphertexts, and how to rotate.

A "secret" in a Lit Action is any string you want the action to use at runtime but never want exposed to callers, observers, or the public IPFS file that holds your action source. Typical examples: third-party API keys (OpenAI, Alchemy, Stripe), database connection strings, signing keys for external services, OAuth client secrets.

Chipotle has no separate secrets store. Secrets are handled with the same primitive as any encrypted data: a PKP wallet acts as a vault, and any Lit Action permitted to use that PKP can call `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

| Concept            | What it is                                                                                                                                  |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **Vault PKP**      | A PKP wallet dedicated to encrypting a set of related secrets. Only actions you've permitted to use this PKP can decrypt its ciphertexts.   |
| **Ciphertext**     | The output of `Lit.Actions.Encrypt({ pkpId, message })`. Safe to store anywhere — IPFS, a database, on-chain, baked into the action source. |
| **Decrypt action** | A Lit Action permitted to use the vault PKP. It calls `Lit.Actions.Decrypt` to recover plaintext, uses it, and returns only the result.     |

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](/management/dashboard) or via [`createWallet`](/management/api_direct). Give it a name that describes the data boundary it protects — e.g. `app-secrets`, `user-alice-vault`, `oracle-api-keys`.

<Note>
  One PKP per logical data boundary is the recommended pattern. See [PKP Wallets as Data Vaults](/lit-actions/patterns#encrypt--decrypt--pkp-wallets-as-data-vaults) for the rationale.
</Note>

### 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 call `Encrypt` 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.

Manage groups through the [Dashboard](/management/dashboard) or directly via the `AccountConfig` contract. See the [Groups guide](/architecture/groups) for the full model.

<Note>
  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).
</Note>

### 3. Encrypt the secret once

Run an action that returns the ciphertext. You only run this when the secret changes.

```javascript theme={null}
// js_params: { pkpId, secret }
async function main({ pkpId, secret }) {
  const ciphertext = await Lit.Actions.Encrypt({ pkpId, message: secret });
  return { ciphertext };
}
```

The returned `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_params` from 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.

```javascript theme={null}
// js_params: { pkpId, encryptedApiKey, city }
async function main({ pkpId, encryptedApiKey, city }) {
  const apiKey = await Lit.Actions.Decrypt({
    pkpId,
    ciphertext: encryptedApiKey,
  });

  const res = await fetch(
    `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}`
  );
  const data = await res.json();

  // Return only what the caller is allowed to see — not the API key.
  return { temp: data?.main?.temp };
}
```

The plaintext exists in memory only during the call, and only inside the action code you wrote and permitted. It's on your action not to return, log, or otherwise leak it.

## Where to put the ciphertext

The ciphertext is safe in the open. Pick the storage option that matches how the secret is consumed:

| Storage                                | When to use                                                                                                                           |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Hardcoded in the action source**     | The secret rarely changes and you want it pinned to a specific action CID. Rotating the secret mints a new CID.                       |
| **`js_params` passed by your backend** | Multiple secrets, or secrets that change without redeploying the action. Your backend stores the ciphertext and supplies it per call. |
| **On-chain registry contract**         | You want anyone (or a permissioned set) to be able to fetch the current ciphertext.                                                   |
| **IPFS / database**                    | Bulk storage of many ciphertexts (e.g. one per user), addressable by some key.                                                        |

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.

```javascript theme={null}
// js_params: { pkpId, encryptedOpenAiKey, encryptedAnthropicKey, prompt }
async function main({ pkpId, encryptedOpenAiKey, encryptedAnthropicKey, prompt }) {
  const [openaiKey, anthropicKey] = await Promise.all([
    Lit.Actions.Decrypt({ pkpId, ciphertext: encryptedOpenAiKey }),
    Lit.Actions.Decrypt({ pkpId, ciphertext: encryptedAnthropicKey }),
  ]);
  // ... use both keys
}
```

**Bundled JSON in one ciphertext.** Useful when secrets are always used together (e.g. an OAuth `client_id` + `client_secret` pair, or an API key + endpoint URL).

```javascript theme={null}
// Encrypt once: JSON.stringify({ clientId, clientSecret }) → ciphertext
// Decrypt at runtime:
const bundle = JSON.parse(
  await Lit.Actions.Decrypt({ pkpId, ciphertext: encryptedBundle })
);
const { clientId, clientSecret } = bundle;
```

The bundled form means one decrypt call instead of N, at the cost of having to re-encrypt the whole bundle to rotate any single field.

## 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.

```
1. Run the encrypt action with the new secret value.
2. Replace the stored ciphertext (in your DB, registry, or action source).
3. Old ciphertexts continue to decrypt to the old value — invalidate them
   on the upstream system (revoke the old API key, etc.).
```

You do not need to rotate the PKP unless you suspect the on-chain permission set has been compromised.

If the ciphertext is **hardcoded in the action source**, rotation mints a new IPFS CID. Update any contracts or callers that pin to the old CID.

## 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](/lit-actions/patterns#securing-rpc-urls--hiding-api-keys-with-encryption) for the full pattern.

## Common mistakes

* **Returning the plaintext secret in the action response.** Whatever you `return` from `main` reaches 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_params` are 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

| Guarantee                                                   | Enforced by                                                                                |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Only permitted IPFS CIDs can use the vault PKP              | On-chain `AccountConfig` contract via group membership                                     |
| The action code that runs is exactly the IPFS CID requested | IPFS content addressing + node verification                                                |
| Who can *call* the action                                   | Your usage API key's group access + your action's own gating logic                         |
| The plaintext is never exposed by a permitted action        | **You** — by auditing the action source against its IPFS CID before adding it to the group |

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](/lit-actions/patterns#gating-logic--aka-access-control-conditions) for patterns.

## See also

* [Encrypt / Decrypt — PKP Wallets as Data Vaults](/lit-actions/patterns#encrypt--decrypt--pkp-wallets-as-data-vaults) — the broader vault pattern, including gated decrypt actions for dApp users.
* [Securing RPC URLs](/lit-actions/patterns#securing-rpc-urls--hiding-api-keys-with-encryption) — the canonical embedded-API-key walkthrough.
* [Lit Actions SDK reference](/lit-actions/chipotle#encryption) — `Encrypt` / `Decrypt` API.
* [Encryption migration notes](/lit-actions/migration/encryption) — how Chipotle's TEE-derived encryption differs from the older BLS threshold model.
