Encrypting data with Seal
Walrus stores blobs publicly: anyone with a blob ID can read the bytes. To keep data confidential, encrypt it before you store it. This tutorial walks through a complete round trip with Seal: encrypt a message, store the ciphertext on Walrus, read it back, and decrypt it, with working TypeScript for both directions.
Seal handles encryption and onchain access control; Walrus handles storage. The two are independent, so this page stays focused on how they fit together and links to the Seal documentation for the full API and security model. For the concepts behind the code, see Data Security.
How the pieces fit
- Encrypt the data with Seal. Seal applies threshold encryption: the decryption key is split across key servers, and no single server can decrypt on its own.
- Store the resulting ciphertext on Walrus. Only encrypted bytes ever reach Walrus.
- Read the ciphertext back from Walrus by its blob ID.
- Decrypt with Seal. The key servers release decryption shares only after
an onchain
seal_approvepolicy confirms the requester is allowed.
Prerequisites
- Node.js and the
@mysten/sui,@mysten/seal, and@mysten/walruspackages. - Testnet SUI for gas and WAL for storage. Acquire WAL with
walrus get-wal. - An access-control policy deployed onchain that exposes a
seal_approvefunction. This tutorial uses the allowlist pattern from the Seal repository. Deployallowlist.move, create an allowlist, and add your address to it. The Move side is documented in full under Using Seal; this page does not duplicate it.
Seal does not decide who may decrypt; your seal_approve function does. The key
servers evaluate it against current onchain state before releasing shares. The
allowlist pattern grants access to a fixed set of addresses, but any Move logic
works: token gating, time locks, or custom conditions. See
access-control options.
Configuration
All four scripts share one Sui client (extended with the Walrus SDK), one Seal client, and the policy IDs you got when you deployed the allowlist. Seal key servers are network-specific, so the client, the policy, and the key servers must all be on the same network.
docs/examples/seal/config.ts. You probably need to run `pnpm prebuild` and restart the site.The keypair helper supplies a Testnet account with SUI and WAL. In a browser app
you would sign with a wallet through @mysten/dapp-kit instead.
docs/examples/seal/funded-keypair.ts. You probably need to run `pnpm prebuild` and restart the site.Encrypt and store
Encryption needs a Seal identity: an arbitrary byte string that must begin
with your policy's namespace so the seal_approve prefix check passes. For the
allowlist pattern the namespace is the allowlist object's ID; appending a random
nonce gives each message a distinct identity under the same policy.
The core call is sealClient.encrypt, which returns the encrypted object to
store and a symmetric backup key you can ignore:
const { encryptedObject } = await sealClient.encrypt({
threshold: THRESHOLD,
packageId: PACKAGE_ID,
id,
data: message,
});
The full script encrypts the message and stores only the ciphertext on Walrus:
docs/examples/seal/encrypt-and-store.ts. You probably need to run `pnpm prebuild` and restart the site.Run it and note the printed blob ID:
SUI_PRIVATE_KEY=suiprivkey1... npx tsx encrypt-and-store.ts
Read and decrypt
Decryption has three moving parts: a SessionKey authorized once by the user's
signature, a PTB that calls seal_approve so the key servers can check the
policy, and the decrypt call itself.
The PTB must have its sender set to the requesting address. The allowlist
policy checks ctx.sender(), so a missing or mismatched sender fails with
Transaction was not signed by the correct sender:
const tx = new Transaction();
tx.setSender(address);
tx.moveCall({
target: `${PACKAGE_ID}::${MODULE_NAME}::seal_approve`,
arguments: [tx.pure.vector('u8', fromHex(id)), tx.object(ALLOWLIST_ID)],
});
const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true });
const decrypted = await sealClient.decrypt({ data: encryptedBytes, sessionKey, txBytes });
The full script reads the blob back from Walrus, recovers the identity from the encrypted object, authorizes a session key, fetches decryption shares, and decrypts:
docs/examples/seal/read-and-decrypt.ts. You probably need to run `pnpm prebuild` and restart the site.Run it with the blob ID from the previous step:
SUI_PRIVATE_KEY=suiprivkey1... npx tsx read-and-decrypt.ts <blobId>
If the requesting address is on the allowlist, the original message prints. If
not, the key servers refuse and the SDK throws NoAccessError.
Troubleshooting
Transaction was not signed by the correct sender— calltx.setSender()with the requester's address before building the PTB, and make sure that address matches the session key's address.Requested package is not supported— the key servers do not recognize your package on this network. Confirm the package is published on the same network as the key servers and thatPACKAGE_IDis correct.NoAccessError— the policy denied access. For the allowlist pattern, confirm the requesting address was added to the allowlist.InvalidParameterright after creating objects — a key server's full node may not have indexed a just-created object yet. Wait a few seconds and retry.
For the complete Seal API, key-server selection, and the security model, see Using Seal.