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

# Encrypt & Decrypt

> Learn how to encrypt a secret and decrypt it using CDR threshold decryption.

This guide walks through the two main CDR flows:

* `uploadCDR` / `accessCDR` for small secrets stored directly on-chain
* `uploadFile` / `downloadFile` for larger encrypted files stored off-chain

### Prerequisites

* [CDR SDK setup](/developers/cdr-sdk/setup) complete with WASM initialized and client created

## What Runs On-Chain vs Off-Chain

| Operation                    | Sends transaction?           | What happens                                                               |
| ---------------------------- | ---------------------------- | -------------------------------------------------------------------------- |
| `observer.getGlobalPubKey()` | No                           | Pure RPC read of DKG state                                                 |
| `uploadCDR()`                | Yes, 2 txs                   | Local TDH2 encryption plus `allocate()` and `write()`                      |
| `uploadFile()`               | Yes, 2 txs + storage upload  | Local AES encryption, storage upload, then `allocate()` and `write()`      |
| `accessCDR()`                | Yes, 1 tx                    | `read()` on-chain, then off-chain partial collection and local combination |
| `downloadFile()`             | Yes, 1 tx + storage download | `accessCDR()` plus encrypted file download and local AES decryption        |

## Encrypt a Secret

The diagram below shows the on-chain secret flow: allocate a vault, encrypt the
secret locally with TDH2, and write the ciphertext to the vault.

<Frame>
  <img src="https://mintcdn.com/story/3cCXy3jIAaxk6hza/images/developers/cdr-sdk/cdr-encryption-flow-text.png?fit=max&auto=format&n=3cCXy3jIAaxk6hza&q=85&s=d9622b896f0d1a9ba69939a13d34b4e5" alt="CDR encryption flow showing vault allocation, local encryption, and writing the ciphertext to the vault" width="1054" height="974" data-path="images/developers/cdr-sdk/cdr-encryption-flow-text.png" />
</Frame>

The simplest "owner-only" pattern uses your wallet (EOA) address as both the
write and read condition. The CDR contract bypasses the condition check when
`msg.sender` equals the configured condition address, so only that wallet can
write or read the vault. Because the high-level `uploadCDR()` helper validates
that condition addresses point at deployed condition contracts, EOA conditions
are configured through the low-level `allocate()` call with
`skipConditionValidation: true`.

```typescript theme={null}
import { initWasm, uuidToLabel } from "@piplabs/cdr-sdk";
import { toHex } from "viem";

await initWasm();

// Assumes `client` and `walletClient` are already created (see Setup)
const { uploader, observer } = client;
const walletAddress = walletClient.account!.address;

// Pure read: fetch the DKG global public key
const globalPubKey = await observer.getGlobalPubKey();

// Encode your secret as bytes
const secret = "my confidential data";
const dataKey = new TextEncoder().encode(secret);

// On-chain transaction: allocate a vault using the wallet address as the
// write AND read condition. Only this EOA can write or read.
const { uuid, txHash: allocateTx } = await uploader.allocate({
  updatable: false,
  writeConditionAddr: walletAddress,
  readConditionAddr: walletAddress,
  writeConditionData: "0x",
  readConditionData: "0x",
  skipConditionValidation: true,
});

// Local: TDH2-encrypt the secret, bound to this vault's UUID
const label = uuidToLabel(uuid);
const ciphertext = await uploader.encryptDataKey({
  dataKey,
  globalPubKey,
  label,
});

// On-chain transaction: write encrypted data to the vault
const { txHash: writeTx } = await uploader.write({
  uuid,
  accessAuxData: "0x",
  encryptedData: toHex(ciphertext.raw),
});

console.log(`Vault created with UUID: ${uuid}`);
console.log(`Allocate tx: ${allocateTx}`);
console.log(`Write tx: ${writeTx}`);
```

<Note>
  You can use any EOA address as a write or read condition. Only that EOA can
  perform the matching action — every other caller reverts because EOAs do not
  implement `checkWriteCondition` / `checkReadCondition`. To restrict one side
  only, set just that side to your wallet address and use a condition contract
  (such as `LicenseReadCondition`) for the other.
</Note>

<Note>
  The high-level `uploadCDR()` helper expects deployed condition contracts on
  both sides, so it does not support EOA conditions. Use it for patterns like
  Story license-gated reads (see [IP Asset
  Vaults](/developers/cdr-sdk/ip-asset-vaults)), and use the low-level
  `allocate()` (with `skipConditionValidation: true`) + `write()` flow shown
  above for owner-only EOA conditions.
</Note>

<Note>The value of the transaction must be exactly the same as the fee.</Note>

<Note>
  `dataKey` is the historical parameter name. In `encryptDataKey()` it can be
  any secret bytes, not just a cryptographic key.
</Note>

<Warning>
  Vault encrypted data is limited to **1024 bytes** on Aeneid
  (`maxEncryptedDataSize`). TDH2 adds overhead, so the maximum plaintext is
  smaller. For larger content, use `uploadFile()` so only a small `{cid, key}`
  payload is written to the vault.
</Warning>

## Decrypt a Secret

Decryption requires submitting a read request on-chain, collecting partial
decryptions from validators, and combining them client-side.

<Frame>
  <img src="https://mintcdn.com/story/3cCXy3jIAaxk6hza/images/developers/cdr-sdk/cdr-decryption-flow-text.png?fit=max&auto=format&n=3cCXy3jIAaxk6hza&q=85&s=4c908e837c122d3a7f0dd04b0056053e" alt="CDR decryption flow showing ephemeral key generation, access control check, partial decryptions from validators, and client-side combination" width="1108" height="854" data-path="images/developers/cdr-sdk/cdr-decryption-flow-text.png" />
</Frame>

```typescript theme={null}
const { consumer } = client;

// Sends 1 transaction, then collects partials and combines them locally
const { dataKey, txHash } = await consumer.accessCDR({
  uuid,
  accessAuxData: "0x",
  timeoutMs: 120_000, // wait up to 2 minutes for validators
});

const secret = new TextDecoder().decode(dataKey);
console.log(`Read tx: ${txHash}`);
console.log(`Decrypted secret: ${secret}`);
```

<Note>
  In `v0.1.1`, `accessCDR()` can auto-generate the ephemeral keypair and
  auto-query `globalPubKey` / `threshold` when you omit them.
</Note>

<Note>
  The timeout of the request on the server side is 200 blocks, which is
  approximately 7 minutes. If you're not able to collect enough partials within
  this timeout, try another read request.
</Note>

<Accordion title="Advanced: Manual Keypair Control" icon="key">
  ```typescript theme={null}
  import { secp256k1 } from "@noble/curves/secp256k1";
  import { toHex } from "viem";

  const { consumer, observer } = client;

  const globalPubKey = await observer.getGlobalPubKey();
  const threshold = await observer.getThreshold();

  const recipientPrivKey = secp256k1.utils.randomPrivateKey();
  const requesterPubKey = toHex(
  secp256k1.getPublicKey(recipientPrivKey, false),
  );

  const { dataKey, txHash } = await consumer.accessCDR({
  uuid,
  accessAuxData: "0x",
  requesterPubKey,
  recipientPrivKey,
  globalPubKey,
  threshold,
  timeoutMs: 120_000,
  });

  console.log(`Read tx: ${txHash}`);
  console.log(new TextDecoder().decode(dataKey));

  ```
</Accordion>

## Encrypt and Download a File

<Columns cols={2}>
  <Frame>
    <img src="https://mintcdn.com/story/3cCXy3jIAaxk6hza/images/developers/cdr-sdk/cdr-encryption-flow-file.png?fit=max&auto=format&n=3cCXy3jIAaxk6hza&q=85&s=3d499a1c349e061ddcfde701f5f56b93" alt="CDR encryption flow showing vault allocation, local encryption, and writing the encrypted key plus data URL to the vault" width="1378" height="1234" data-path="images/developers/cdr-sdk/cdr-encryption-flow-file.png" />
  </Frame>

  <Frame>
    <img src="https://mintcdn.com/story/3cCXy3jIAaxk6hza/images/developers/cdr-sdk/cdr-decryption-flow-file.png?fit=max&auto=format&n=3cCXy3jIAaxk6hza&q=85&s=dd18da8faa8063a127799b09b3631030" alt="CDR decryption flow showing ephemeral key generation, access control check, partial decryptions from validators, and client-side combination" width="1560" height="1378" data-path="images/developers/cdr-sdk/cdr-decryption-flow-file.png" />
  </Frame>
</Columns>

Use the file workflow when the encrypted payload should live off-chain and only
the encrypted file key plus pointer should be stored in the vault.

Upload happens once by the data owner. Download happens later by an authorized
reader who recovers the vault payload and then decrypts the stored file.

The `uploadFile()` helper requires deployed condition contracts on both sides,
so the example below uses Story's `OwnerWriteCondition` for the write side and
`LicenseReadCondition` for the read side. License token holders can decrypt
the file (see [IP Asset Vaults](/developers/cdr-sdk/ip-asset-vaults) for the
end-to-end license setup). For an owner-only file flow, replicate the
low-level steps shown earlier with your wallet (EOA) address as both
conditions.

```typescript theme={null}
import { HeliaProvider } from "@piplabs/cdr-sdk";
import { readFile, writeFile } from "node:fs/promises";
import { createHelia } from "helia";
import { unixfs } from "@helia/unixfs";
import { CID } from "multiformats/cid";
import { encodeAbiParameters } from "viem";

const uploaderAddress = walletClient.account!.address;
const OWNER_WRITE_CONDITION = "0x4C9bFC96d7092b590D497A191826C3dA2277c34B";
const LICENSE_READ_CONDITION = "0xC0640AD4CF2CaA9914C8e5C44234359a9102f7a3";
const LICENSE_TOKEN = "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC";

const writeConditionData = encodeAbiParameters(
  [{ type: "address" }],
  [uploaderAddress],
);

const readConditionData = encodeAbiParameters(
  [{ type: "address" }, { type: "address" }],
  [LICENSE_TOKEN, ipId],
);

// Pure read
const globalPubKey = await client.observer.getGlobalPubKey();

const helia = await createHelia();
const storage = new HeliaProvider({
  helia,
  unixfs: unixfs(helia),
  CID: (s) => CID.parse(s),
});

const sourceFile = await readFile("./example.pdf");

// Off-chain upload + 2 on-chain transactions
const { uuid, cid } = await client.uploader.uploadFile({
  content: new Uint8Array(sourceFile),
  storageProvider: storage,
  globalPubKey,
  updatable: false,
  writeConditionAddr: OWNER_WRITE_CONDITION,
  readConditionAddr: LICENSE_READ_CONDITION,
  writeConditionData,
  readConditionData,
  accessAuxData: "0x",
});

// 1 on-chain read transaction + off-chain download + local AES decryption
const { content, txHash } = await client.consumer.downloadFile({
  uuid,
  accessAuxData: "0x",
  storageProvider: storage,
  timeoutMs: 120_000,
});

console.log(`Stored at CID: ${cid}`);
console.log(`Read tx: ${txHash}`);
await writeFile("./example.decrypted.pdf", Buffer.from(content));
console.log("Decrypted file written to ./example.decrypted.pdf");
```

<Note>
  `HeliaProvider` is the only storage backend fully tested on Aeneid in the
  current release, and it requires Node.js 22+.
</Note>

<Note>
  `uploadFile()` and `downloadFile()` work with raw file bytes. In a browser,
  start from a `File` object and convert it with `new Uint8Array(await
      file.arrayBuffer())`.
</Note>

### Storage Providers

The encrypted-file workflow supports four storage backends:

* `HeliaProvider` for in-process IPFS. This is the best starting point for
  development and the only backend fully tested on Aeneid so far.
* `GatewayProvider` for an external IPFS HTTP API plus a gateway URL.
* `StorachaProvider` for Storacha / `web3.storage`.
* `SynapseProvider` for Filecoin-backed storage via Synapse.

<Note>
  If you use `HeliaProvider`, pass the `CID.parse` function into the constructor
  as shown above to avoid class mismatches.
</Note>

## Step-by-Step (Low-Level)

If you need more control over the process, you can call each step individually.

<Note>
  These snippets continue from the variables in the examples above:
  `walletClient`, `globalPubKey`, `threshold`, `requesterPubKey`,
  `recipientPrivKey`, and `dataKey`.
</Note>

### Encrypt (Low-Level)

```typescript theme={null}
import { uuidToLabel } from "@piplabs/cdr-sdk";
import { toHex } from "viem";

const walletAddress = walletClient.account!.address;

// On-chain transaction: allocate a vault using the wallet address as the
// write AND read condition. Only this EOA can write or read.
const { txHash: allocateTx, uuid } = await uploader.allocate({
  updatable: false,
  writeConditionAddr: walletAddress,
  readConditionAddr: walletAddress,
  writeConditionData: "0x",
  readConditionData: "0x",
  skipConditionValidation: true,
});

// Local: derive the label from the UUID
const label = uuidToLabel(uuid);

// Local: TDH2 encrypt the secret
const ciphertext = await uploader.encryptDataKey({
  dataKey,
  globalPubKey,
  label,
});

// On-chain transaction: write encrypted data to the vault
const { txHash: writeTx } = await uploader.write({
  uuid,
  accessAuxData: "0x",
  encryptedData: toHex(ciphertext.raw),
});
```

### Decrypt (Low-Level)

```typescript theme={null}
import { uuidToLabel } from "@piplabs/cdr-sdk";

// On-chain transaction: submit read request
const { txHash: readTx } = await consumer.read({
  uuid,
  accessAuxData: "0x",
  requesterPubKey,
});

// Get the block number for polling
const receipt = await publicClient.getTransactionReceipt({ hash: readTx });

// Off-chain/network: collect partial decryptions from validators
const partials = await consumer.collectPartials({
  uuid,
  minPartials: threshold,
  fromBlock: receipt.blockNumber,
  timeoutMs: 120_000,
  requesterPubKey, // required if you use dkgSource: "cosmos-abci"
});

// Pure read: fetch the vault ciphertext
const label = uuidToLabel(uuid);
const vault = await observer.getVault(uuid);

// Local: decrypt each partial, then combine them
const recoveredDataKey = await consumer.decryptDataKey({
  ciphertext: {
    raw: Uint8Array.from(Buffer.from(vault.encryptedData.slice(2), "hex")),
    label,
  },
  partials,
  recipientPrivKey,
  globalPubKey,
  label,
  threshold,
});
```

## Query DKG State

You can query DKG state and fees without a wallet or WASM initialization:

```typescript theme={null}
import { createPublicClient, http } from "viem";
import { CDRClient } from "@piplabs/cdr-sdk";

const publicClient = createPublicClient({
  transport: http("https://aeneid.storyrpc.io"),
});
const client = new CDRClient({ network: "testnet", publicClient });

const threshold = await client.observer.getOperationalThreshold();
console.log("Operational threshold:", threshold);

const [allocateFee, writeFee, readFee] = await Promise.all([
  client.observer.getAllocateFee(),
  client.observer.getWriteFee(),
  client.observer.getReadFee(),
]);
console.log(
  `Fees — allocate: ${allocateFee}, write: ${writeFee}, read: ${readFee}`,
);

// Query a specific vault
const vault = await client.observer.getVault(1);
console.log("Vault:", vault);
```

## Understanding Fees

Each CDR operation has an on-chain fee:

| Operation | Fee Query                   | Description                      |
| --------- | --------------------------- | -------------------------------- |
| Allocate  | `observer.getAllocateFee()` | One-time cost to create a vault  |
| Write     | `observer.getWriteFee()`    | Cost per write to a vault        |
| Read      | `observer.getReadFee()`     | Cost per read/decryption request |

Fees are paid in native tokens (wei) and are sent as `msg.value` with each transaction.
