Sui seal
Skill widnyana/eyay-toolkits/plugins/sui-dev-tools/skills/sui-seal
Agents don’t behave, they swarm. Skills and plugins slot in mid flight, and somehow the whole thing is a mess
npx -y skills add widnyana/eyay-toolkits --skill sui-sealAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Seal — decentralized secrets management on Sui. Identity-based encryption with onchain access policies enforced by threshold key servers. Use when building apps that encrypt user data (stored on Walrus, Sui objects, or off-chain) and gate decryption on Move-defined policies. Covers the Move side (`seal_approve*` patterns) and the `@mysten/seal` TypeScript SDK.
SKILL.md
19.5 KB, as published. Nobody here has run it
Seal Skill
Integrate Seal — Mysten Labs' decentralized secrets management service. Seal encrypts data client-side with identity-based encryption (IBE) and delegates decryption-key release to a threshold of off-chain key servers that evaluate onchain access policies written in Move. Storage is orthogonal: ciphertext lives wherever needed (Walrus, Sui objects, S3). Follow these rules precisely.
Not for: wallet private keys, regulated PHI, classified/government secrets. Seal's trust model assumes
t-of-nkey servers are honest — it is not suitable when a single compromised server must never leak plaintext. See the Security section.
This skill covers both sides of a Seal integration: the Move package that defines who can decrypt, and the TypeScript SDK that performs encryption and decryption.
1. Mental model (read first)
Seal is not "encrypt with a key and store the key somewhere." The flow is:
- The Move package defines one or more
seal_approve*entry functions. Each takes an identityid: vector<u8>plus any objects it needs, and aborts if access is denied. - Encrypt data client-side against a chosen identity
idand a set of key servers with a thresholdt. The SDK produces ciphertext and a symmetric key. - To decrypt, the client builds a PTB that calls the
seal_approve*function and askstkey servers to release a derived decryption key. Each key server dry-runs the PTB on a full node; if the function does not abort, the server returns its share. - The client reconstructs the decryption key from
tshares and decrypts locally.
Key consequences:
- Access policy changes take effect immediately for future decryptions — no re-encryption needed. Just edit the Move package (or the on-chain state it reads).
seal_approve*is evaluated viadry_run_transaction_blockon a full node. It must be side-effect free, cannot useRandom, and must not rely on just-created objects (propagation delay).- The identity namespace is scoped by package ID: Seal prepends the full package ID to every key-id, so packages cannot forge each other's identities. Within the package, design the rest of the id bytes.
- Once a user has fetched a decryption key, it cannot be revoked — the key is a cryptographic object held locally by the user. Plan access-control changes around this.
2. Install
npm install @mysten/seal @mysten/sui
The SDK depends on @mysten/sui (v2+). Imports all come from the package root:
import {
SealClient,
SessionKey,
EncryptedObject,
NoAccessError,
type KeyServerConfig,
type SealClientOptions,
type EncryptOptions,
type DecryptOptions,
} from '@mysten/seal';
There are no subpath exports — do not write @mysten/seal/client or similar.
3. The Move side: seal_approve* functions
3.1 Function shape
Every access policy is a Move function whose name starts with seal_approve. Hard rules:
- Must be
entry, notpublicand notpublic entry. (Public would lock the signature against future SDK changes.) - First parameter must be
id: vector<u8>— the identity the client is decrypting against. - Must abort when access is denied; must not return a value. Return is
(). - Must be side-effect free: no
transfer, no mutating shared state, no event emission. The function is evaluated viadry_run_transaction_block— effects would be discarded anyway, and key servers may reject functions that appear to mutate. - Must not use
sui::random::Random: its output is non-deterministic across dry-runs on different servers and a leaked random value can defeat policy. - The
Clockobject is fine for time checks.
module my_pkg::policies;
const ENoAccess: u64 = 0;
// ✅ Correct shape.
entry fun seal_approve(id: vector<u8>, policy: &MyPolicy, ctx: &TxContext) {
assert!(check(id, policy, ctx.sender()), ENoAccess);
}
// ❌ Wrong: public means you can never change the signature without breaking callers.
public entry fun seal_approve_bad(id: vector<u8>, /* ... */) { /* ... */ }
// ❌ Wrong: returning a bool — must assert + abort.
entry fun seal_approve_bool(id: vector<u8>): bool { true }
3.2 Key-id conventions
Seal prepends the full package id to the client-supplied id bytes when evaluating policies. That is, the effective identity is [package_id][your_id_bytes]. The seal_approve* function therefore only needs to validate your_id_bytes.
A common pattern is [policy_object_id][nonce] or [creator_address][nonce]. Design the id so that:
- A caller cannot reuse another user's id (prefix with owner / policy object id).
- Multiple encryptions under the same policy are distinguishable (append a nonce).
- The prefix the
seal_approve*function checks is unforgeable — an attacker encrypting under a different id must be detected.
3.3 The five canonical patterns
Reference implementations live at MystenLabs/seal/move/patterns/sources. Use them as starting points:
| Pattern | File | Use case |
|---|---|---|
| Private data | private_data.move | Only the object's current owner can decrypt. Personal storage, private NFTs. |
| Whitelist / Allowlist | whitelist.move | An admin maintains a Table<address, bool>; anyone on it can decrypt. Subscriptions, early access. |
| Subscription | subscription.move | Users mint a time-limited pass; decryption allowed while pass is valid. Paid media, SaaS. |
| Time-lock encryption (TLE) | tle.move | Identity encodes a timestamp; anyone can decrypt once clock.timestamp_ms() >= t. Sealed-bid auctions, scheduled reveals. |
| Voting | voting.move | Encrypt ballots, reveal via onchain decryption once voting ends. Pairs with seal::bf_hmac_encryption. |
Whitelist / private-data patterns both enforce a key-id prefix check:
// whitelist.move (simplified): id must start with the whitelist's object id.
fun check_policy(id: vector<u8>, wl: &Whitelist, caller: address): bool {
// 1. The id's prefix must equal this whitelist's object id — otherwise the caller
// could try to use this approve function to unlock a *different* whitelist's data.
let wl_id = object::id(wl).to_bytes();
if (!has_prefix(&id, &wl_id)) return false;
// 2. The caller must be on the list.
wl.addresses.contains(caller)
}
3.4 Testing policies
seal_approve* functions are ordinary Move entry functions — test them with the standard Move test framework. Dry-run them via sui client dry-run to confirm behavior against real on-chain state before shipping.
4. The TypeScript side
4.1 SealClient setup
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { SealClient } from '@mysten/seal';
const suiClient = new SuiGrpcClient({ url: 'https://fullnode.testnet.sui.io:443' });
const seal = new SealClient({
suiClient,
serverConfigs: [
{ objectId: '0x...mysten-server-1', weight: 1 },
{ objectId: '0x...ruby-nodes', weight: 1 },
{ objectId: '0x...nodeinfra', weight: 1 },
],
verifyKeyServers: false, // see below
});
Picking servers. Seal is permissionless; anyone can run a key server. The verified-provider list (Testnet and Mainnet) lives at the Pricing page. Choose the set and the threshold t. Changing the server set requires re-encrypting the data (different public keys) — use envelope encryption (§5) if rotation without re-encrypting blobs is needed.
verifyKeyServers. Anyone can register an on-chain KeyServer object with any URL. Setting verifyKeyServers: true makes the SDK fetch /v1/service on each server at startup and confirm the object id matches. Do this once at startup in long-lived services; in short-lived contexts (serverless, frontend) prefer false for latency, and verify out-of-band.
Weights. weight biases which servers are asked first but every server in the set is a valid source. For decentralized (committee) key servers, pass the single committee object id and one aggregatorUrl.
API keys. Some permissioned servers require an HTTP header:
{ objectId: '0x...', weight: 1, apiKeyName: 'x-api-key', apiKey: process.env.SEAL_API_KEY }
4.2 Encryption
const { encryptedObject, key } = await seal.encrypt({
threshold: 2, // t in t-of-n
packageId: '0x...my_pkg', // the package that owns the seal_approve* functions
id: keyIdBytes, // Uint8Array — the policy-specific part of the identity
data: new TextEncoder().encode('hello'),
});
encryptedObjectis aUint8Array— store it wherever (Walrus is the obvious fit).keyis the symmetric key used under the hood. Discard it unless there is an explicit disaster-recovery reason. Keeping it means anyone who obtains it decrypts the data with no policy check.encryptdoes not hide the message size. Pad if size is sensitive.- To inspect a stored blob later:
EncryptedObject.parse(bytes)returns the threshold, package id, id, and ciphertext metadata.
4.3 Decryption: SessionKey
Decryption requires a SessionKey — a short-lived ephemeral keypair the user signs once to authorize the SDK to fetch multiple decryption keys without re-prompting.
import { SessionKey } from '@mysten/seal';
const session = await SessionKey.create({
address: currentAccount.address,
packageId: '0x...my_pkg', // session is scoped to a single package
ttlMin: 10, // minutes; max is set by the key servers
suiClient,
});
// Ask the wallet to sign the session's personal message.
const { signature } = await signPersonalMessage({ message: session.getPersonalMessage() });
await session.setPersonalMessageSignature(signature);
Notes:
- The session is scoped to one
packageId. It cannot be reused against another package'sseal_approve*. - A
Signercan be passed in the constructor (backend/CLI flows) to skip the personal-message round-trip. SessionKeyis serializable viasession.export()and can be persisted (e.g., IndexedDB) across tabs.
Once the session is initialized, build the PTB that calls the policy and decrypt:
import { Transaction } from '@mysten/sui/transactions';
const tx = new Transaction();
tx.moveCall({
target: `${PKG}::policies::seal_approve`,
arguments: [tx.pure.vector('u8', keyIdBytes), tx.object(policyObjectId)],
});
const plaintext = await seal.decrypt({
data: encryptedObject,
sessionKey: session,
txBytes: await tx.build({ client: suiClient, onlyTransactionKind: true }),
});
Critical:
- All
moveCalls in the PTB must target the same package id as the session. - Inside
seal_approve*,TxContext::sender()returns the session key's address, not a wallet address — design policies accordingly. - Fully specify objects in the PTB (include versions via
tx.objectRef(...)when available). Key servers dry-run the PTB and freshly-created or just-mutated objects may not be visible yet.
4.4 Batch decryption: fetchKeys
When decrypting many blobs encrypted under different identities, do not call decrypt in a loop — that's N × t key-server requests. Instead, build one PTB with multiple seal_approve* calls and use fetchKeys:
const tx = new Transaction();
for (const idBytes of allIds) {
tx.moveCall({
target: `${PKG}::policies::seal_approve`,
arguments: [tx.pure.vector('u8', idBytes), tx.object(policyObjectId)],
});
}
await seal.fetchKeys({
ids: allIds,
sessionKey: session,
txBytes: await tx.build({ client: suiClient, onlyTransactionKind: true }),
threshold: 2,
});
// Now per-blob decrypts are local — no more key-server traffic.
for (const { encryptedObject } of blobs) {
const pt = await seal.decrypt({ data: encryptedObject, sessionKey: session, txBytes: /* same PTB */ });
}
SealClient caches fetched keys internally, so reuse the same instance across requests in the application.
4.5 Error handling
Common errors:
NoAccessError— theseal_approve*function aborted. The user is not authorized.InvalidParameter— often means an object referenced in the PTB isn't visible to the full node yet. Wait a few seconds and retry.RetryableKeyServerError— retry on the same or a different server.
Wrap decrypt in a retry with bounded backoff for transient errors; don't retry NoAccessError.
5. Envelope encryption (the pattern that matters with Walrus)
Seal.encrypt works well for small payloads but has two limits for large/long-lived data:
- IBE encryption over many MB is slow.
- To rotate key servers, re-encrypting every blob is required.
Envelope encryption (a.k.a. layered / DEK-KEK):
// 1. Encrypt the data with a fresh symmetric key (fast, symmetric).
const dek = crypto.getRandomValues(new Uint8Array(32));
const ciphertext = await aesGcmEncrypt(dek, plaintext); // your preferred AES-GCM helper
// 2. Wrap the DEK with Seal — small payload, policy-gated.
const { encryptedObject: wrappedDek } = await seal.encrypt({
threshold: 2,
packageId: PKG,
id: keyIdBytes,
data: dek,
});
// 3. Store `ciphertext` on Walrus, store `wrappedDek` alongside (or on Sui).
To rotate key servers later: re-encrypt only the small wrappedDek; the large ciphertext on Walrus stays put. This is the recommended pattern for any Walrus-backed storage.
Default to AES-256-GCM for the data-encryption step. Reserve HMAC-CTR for onchain decryption (§6) — it is slower and more memory-hungry.
6. Onchain decryption (voting, sealed-bid auctions)
For use cases where the Move contract itself needs to decrypt (e.g., tally encrypted votes on-chain), Seal publishes a companion Move package:
- Testnet:
0x4016869413374eaa71df2a043d1660ed7bc927ab7962831f8b07efbc7efdb2c3 - Mainnet:
0xcb83a248bda5f7a0a431e6bf9e96d184e604130ec5218696e3f1211113b447b7
Module: seal::bf_hmac_encryption. Encryption must use HMAC-CTR (not AES) for the ciphertext to be decryptable on-chain.
Three-step flow:
- Initialization — fetch key-server public keys via
client.getPublicKeys, convert withbf_hmac_encryption::new_public_key, and store them as a shared object. - Derived-key delivery — client calls
client.getDerivedKeysagainst the post-reveal condition and submits the keys + proofs to the contract, which verifies them viaverify_derived_keys. - Decryption — contract calls
bf_hmac_encryption::decrypt(ciphertext, derived_keys, public_keys)which returnsOption<vector<u8>>.
Only reach for this when plaintext truly needs to be visible on-chain. For everything else, decrypt client-side.
7. Security best practices
-
Threshold choice. Pick
tto balance availability vs. confidentiality.2-of-3tolerates one offline server;3-of-5tolerates two. A too-hightrisks permanent data loss if servers disappear — not a hypothetical, because policies are the only source of truth for access but the servers are the only source of keys. -
Vet operators. Key servers are permissionless. For production, pick operators with real SLAs, and check whether their full-node dependency is self-hosted, third-party, or public — key fetches can hang if the full node does. Use the curated verified providers list as a starting point.
-
Client-side decryption is final. Once the SDK hands the plaintext to the application, Seal has no visibility into what happens next. A compromised frontend or a user copy-pasting the plaintext is out of scope. Add application-level audit logging if needed — key servers do not log deliveries on-chain.
-
Don't use Seal for:
- Wallet private keys, seed phrases, or anything whose compromise is unrecoverable.
- Regulated PHI, PII under HIPAA/GDPR Article 9, or anything requiring certified HSM-level guarantees.
- Secrets that must survive a compromise of
toperators colluding.
-
Symmetric key returned by
encrypt. Thekeyfield is the data-encryption key. If persisted for disaster recovery, treat it like a root secret. Default: drop it. -
Package upgrades are visible on-chain. If the
seal_approve*policy changes via a package upgrade, that change is public. Design around it: don't rely on policies that would be embarrassing to publish.
8. Performance checklist
- Reuse a single
SealClientinstance — it caches derived keys per session. - Reuse a single
SessionKeyfor the user's session (persist in IndexedDB for multi-tab). - Use
fetchKeyswith a multi-call PTB when decrypting many blobs. -
verifyKeyServers: falsein hot paths; verify out-of-band. - Always include full object refs (with version) in PTBs submitted to key servers.
- Use AES-256-GCM (default), not HMAC-CTR, unless onchain decryption is needed.
- Use envelope encryption for payloads >100 KB or for Walrus-backed storage.
9. Seal CLI (one-off ops, key server operators)
seal-cli (from the MystenLabs/seal Rust crate) is useful for debugging and key-server operators — not typically called from app code. Common commands:
seal-cli genkey— produce a masterkey / publickey pair.seal-cli encrypt-aes --threshold N --public-keys ... --package-id ... --id ...seal-cli parse <hex>— decode anEncryptedObjectfor inspection. This is the easiest way to verify what a blob on Walrus was encrypted against.seal-cli fetch-keys— exercise a live key server from the command line.
Encryption is randomized: re-running encrypt-aes on the same input produces different ciphertext. That is expected, not a bug.
10. Routing: what else to load
| Task | Also load |
|---|---|
Writing the seal_approve* Move module | sui-move skill (especially sui-move-object, sui-move-patterns) |
Building PTBs that call seal_approve* for decryption | sui-ts-sdk skill |
| Wiring encryption/decryption into a React dApp | sui-frontend + sui-ts-sdk |
| Storing the ciphertext on Walrus | sui-walrus skill (envelope-encryption pattern is canonical there) |
11. Anti-patterns to avoid
- ❌
public entry fun seal_approve— useentry, notpublic entry. Locks the signature. - ❌
seal_approvethat returnsbool— must abort, not return. - ❌ Using
sui::randominsideseal_approve*— non-deterministic across servers, may also be insecure in a dry-run context. - ❌ Calling
transfer::*, emitting events, or writing to shared state insideseal_approve*. - ❌ Encrypting a large blob directly with
seal.encryptinstead of envelope-encrypting it. - ❌ Calling
seal.decryptin a loop — usefetchKeyswith a batched PTB. - ❌ Re-creating
SealClient/SessionKeyper request — destroys caching. - ❌ Trusting a
KeyServerobject id without verifying the URL → public-key binding (setverifyKeyServers: trueduring setup at least once). - ❌ Assuming access can be revoked after the key is fetched — it cannot.
- ❌ Storing Seal's returned symmetric
keywithout a concrete recovery reason. - ❌ Using HMAC-CTR for app data instead of AES — HMAC-CTR is reserved for onchain decryption.