đĒ Access Control
By default, encrypting a file through the Lighthouse Files Dapp uses Lighthouse's seal_encryption_lighthouse::allowlist package to control who can decrypt it. If you want full control over access logic â a subscription check, a token-gate, a DAO vote, or any custom rule â you can deploy your own Move package with a seal_approve function and use that as the access policy instead. Lighthouse only needs the resulting encrypted bytes; it doesn't need to own the policy.
How Access Control Worksâ
- You write and deploy a Move package with at least one
seal_approvefunction. This is your access policy and SEAL evaluates it onchain to decide whether a given requester can get the decryption key. - You encrypt your data with the SEAL SDK, passing your package's ID (not Lighthouse's) as
packageId. - You upload the resulting ciphertext to Lighthouse the blob itself is public; only the decryption key is gated.
- A recipient decrypts by building a transaction that calls your
seal_approvefunction, which SEAL's key servers evaluate before releasing key shares.
Step 1: Write your own access policyâ
A seal_approve function is a normal Move entry function with one hard requirement: its first parameter must be the requested identity, id: vector<u8> (with the package-ID prefix already stripped by SEAL). If access should be denied, the function must abort â it can't just return false.
Some example Move Patterns
You're free to make this as simple or as complex as you like â check an NFT, a subscription expiry, a whitelist, a DAO vote, whatever your app needs. See SEAL's Access Policy Example Patterns for allowlist, subscription, and time-lock reference implementations.
A few constraints SEAL imposes on every seal_approve function:
- It must be side-effect free, it cannot mutate onchain state.
- Don't use the
Randommodule inside it, its output isn't secure or deterministic across full nodes. - Prefer non-public
entryfunctions and versioned shared objects, so you can upgrade the policy later without breaking existing encrypted data.
Deploy it with the Sui CLI:
sui move build
sui client publish
Note the published package ID â you'll need it for encryption and for every future decryption call.
Step 2: Set up a SealClientâ
Point the client at a set of key servers (a fixed set is the common approach; testnet verified servers are listed in SEAL's pricing/verified servers page):
import { SuiClient, getFullnodeUrl } from "@mysten/sui/client";
import { SealClient } from "@mysten/seal";
const suiClient = new SuiClient({ url: getFullnodeUrl("testnet") });
const client = new SealClient({
suiClient,
serverConfigs: [
{
objectId:
"0xb012378c9f3799fb5b1a7083da74a4069e3c3f1c93de0b27212a5799ce1e1e98",
aggregatorUrl: "https://seal-aggregator-testnet.mystenlabs.com",
weight: 1,
},
],
verifyKeyServers: false, // set true if you want to validate server URLs at startup
});
Step 3: Encrypt against your own packageâ
This is the only real difference from the Lighthouse-managed flow â packageId points at your deployed package, not Lighthouse's allowlist package. id is the identity your seal_approve function will check against (design this however your policy needs â an object ID, an address, a namespace + nonce, etc.):
const { encryptedObject: encryptedBytes, key: backupKey } =
await client.encrypt({
threshold: 2, // number of key servers required to reconstruct the key
packageId: MY_PACKAGE_ID,
id: myIdentityBytes,
data: fileBytes,
});
thresholdis how many of your configured key servers must agree before a decryption key is released.key(the backup symmetric key) is optional to keep â retain it only if you want a disaster-recovery path viaseal-cli'ssymmetric-decrypt.- Encryption doesn't hide the size of
data. Pad with zeros first if size itself is sensitive.
Step 4: Upload the Encrypted Fileâ
The encryptedObject returned by SEAL is just bytes at this point â upload it to Walrus the same way you'd upload any file through Lighthouse, using the Upload a File guide with storageType: "walrus".
import lighthouse from "@lighthouse-web3/sdk";
/**
* Upload a file or folder to Walrus on Lighthouse.
*
* @param {string} path - Location of your file or folder.
* @param {string} apiKey - Your Lighthouse API key.
* @param {object} options - Must include storageType: `"walrus"`.
*/
const uploadResponse = await lighthouse.upload(
encryptedObject,
"YOUR_API_KEY_HERE",
{
storageType: "walrus",
},
);
console.log(uploadResponse);
/* Sample response
{
data: {
Name: 'wow.jpg',
Hash: 'QmUHDKv3NNL1mrg4NTW4WwJqetzwZbGNitdjr2G6Z5Xe6s',
Size: '31735'
}
}
*/
uploadResponse.data.Hash is the CID of the encrypted file on Walrus â this is what you'll use to download and decrypt it later.
See the Upload a File page for the full set of upload options. Since encryption happens entirely on the client side before this call, Lighthouse never has access to the plaintext contents of your file.
Step 5: Decryptâ
Decryption always goes through your package now, since that's what holds the policy.
Create a session key (one wallet signature per package, valid for the TTL you set):
import { SessionKey } from "@mysten/seal";
const sessionKey = await SessionKey.create({
address: suiAddress,
packageId: MY_PACKAGE_ID,
ttlMin: 10,
suiClient,
});
const message = sessionKey.getPersonalMessage();
const { signature } = await keypair.signPersonalMessage(message); // wallet prompt
sessionKey.setPersonalMessageSignature(signature);
Build a transaction that calls your seal_approve function and decrypt:
import { Transaction } from "@mysten/sui/transactions";
import { fromHex } from "@mysten/sui/utils";
const tx = new Transaction();
tx.moveCall({
target: `${MY_PACKAGE_ID}::simple_policy::seal_approve`,
arguments: [
tx.pure.vector("u8", fromHex(myIdentityHex)),
tx.object(accessConfigId), // whatever objects your policy needs
],
});
const txBytes = await tx.build({
client: suiClient,
onlyTransactionKind: true,
});
const decryptedBytes = await client.decrypt({
data: encryptedBytes,
sessionKey,
txBytes,
});
SEAL evaluates this exactly as if the session-key holder had sent the transaction â ctx.sender() inside seal_approve resolves to the session key's address. If your policy's assert! fails, the key server refuses to release its share and decryption fails; it never partially succeeds.
For fetching several keys at once (e.g. decrypting a batch of files under the same policy), use fetchKeys with a multi-call PTB instead of calling decrypt in a loop â it cuts down round trips to the key servers considerably.
Why you'd do this over Lighthouse's packageâ
- Custom logic â token-gating, subscriptions, time locks, DAO votes â anything expressible in Move, not just an allowlist.
- No dependency on Lighthouse's contract upgrades â you own and control your policy's lifecycle and versioning.
- Composability â your policy can reference your own existing on-chain objects (memberships, NFTs, escrow state) directly.
The tradeoff is you're responsible for your package's correctness and upgrade path â a bug in seal_approve either locks out legitimate users or leaks access, and SEAL can't help you recover from either.