seal

For the complete documentation index, see llms.txt

Using Seal

Use this guide to learn how to protect your app and user data with Seal.

:::tip

Read the Seal Design document first to understand the underlying architecture and concepts before using this guide.

:::

Access control management

Packages should define seal_approve* functions in their modules to control access to the keys associated with their identity namespace. Guidelines for defining seal_approve* functions:

See Example patterns for additional examples and high-level patterns.

As seal_approve* functions are standard Move functions, they can be tested locally using Move tests. Building and publishing the code can be done using the Sui CLI, for example:

cd examples/move
sui move build
sui client publish

Limitations

The seal_approve* functions are evaluated on full nodes using the dry_run_transaction_block RPC call. This call executes the associated Move code using the full node’s local view of the chain state. Because full nodes operate asynchronously, the result of dry_run_transaction_block can vary across nodes based on differences in their internal state.

When using seal_approve* functions, keep the following in mind:

public fun increment(counter: &mut Counter) { counter.count = counter.count + 1; }

entry fun seal_approve(id: vector, cnt1: &Counter, cnt2: &Counter) { assert!(cnt1.count == cnt2.count, ENoAccess); ... }


- `seal_approve*` functions must be side-effect free and cannot modify onchain state.
- Although the `Random` module is available, its output is not secure and not deterministic across full nodes. Avoid using it within `seal_approve*` functions.
- During Seal evaluation, only `seal_approve*` functions can be invoked directly. These functions should not assume composition with other [PTB (Programmable Transaction Block)](https://docs.sui.io/concepts/transactions/prog-txn-blocks) commands.

## `SealClient` setup

The recommended way to encrypt and decrypt the data is to use the [Seal SDK](https://www.npmjs.com/package/@mysten/seal).

First, the app must select the set of key servers it intends to use. Each key server registers its name, public key, and URL onchain by creating a `KeyServer` object. To reference a key server, use the object ID of its corresponding `KeyServer`. A common approach for app developers is to use a fixed, preconfigured set of key servers within their app. Alternatively, the app can support a dynamic selection of key servers, for example, allowing users to choose which servers to use. In this case, the app should display a list of available key servers along with their URLs. After the user selects one or more servers, the app must verify that each provided URL corresponds to the claimed key server (see `verifyKeyServers` in the following section).

A key server can be used multiple times to enable weighting, which allows the app to specify how many times a key server can contribute towards reaching the decryption threshold. This is useful for scenarios where some key servers are more reliable or trusted than others, or when the app wants to ensure that certain key servers are always included in the decryption process.

:::info

Anyone can create an onchain `KeyServer` object that references a known URL (such as `seal.mystenlabs.com`) but uses a different public key. To prevent impersonation, the SDK can perform a verification step: it fetches the object ID from the server's `/v1/service` endpoint and compares it with the object ID registered onchain.

:::

### Choosing key servers

Select key server object IDs from the [verified key servers](/Pricing#verified-key-servers) available in each environment.

Seal supports two server types, **independent** and **decentralized** (committee mode), which you can use individually or in combination. To understand the differences of server types and use cases, see [Seal Server Overview](/ServerOverview).

:::info

Each key server (whether independent or decentralized) counts as one server in your threshold configuration and integrates through the same `SealClient` interface.

:::

#### Option 1: Decentralized-only

Use a single decentralized server to get built-in distributed trust and rotation support.

```typescript
const suiClient = new SuiClient({ url: getFullnodeUrl('testnet') });

const client = new SealClient({
  suiClient,
  serverConfigs: [
    // Decentralized server from https://seal-docs.wal.app/Pricing#verified-decentralized-key-servers
    {
      objectId: "0xb012378c9f3799fb5b1a7083da74a4069e3c3f1c93de0b27212a5799ce1e1e98",
      aggregatorUrl: "https://seal-aggregator-testnet.mystenlabs.com",
      weight: 1,
    },
  ],
  verifyKeyServers: false,
});

Option 2: Independent-only

Direct operator model with simpler infrastructure relationships.

const suiClient = new SuiClient({ url: getFullnodeUrl('testnet') });

const client = new SealClient({
  suiClient,
  serverConfigs: [
    // Independent servers from https://seal-docs.wal.app/Pricing#verified-independent-server-type-key-servers
    {
      objectId: "0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75",
      weight: 1,
    },
    {
      objectId: "0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8",
      weight: 1,
    }
  ],
  verifyKeyServers: false,
});

Option 3: Hybrid (decentralized + independent)

Flexible trust distribution and cost control.

const suiClient = new SuiClient({ url: getFullnodeUrl('testnet') });

const client = new SealClient({
  suiClient,
  serverConfigs: [
    // Decentralized server from https://seal-docs.wal.app/Pricing#verified-decentralized-key-servers
    {
      objectId: "0xb012378c9f3799fb5b1a7083da74a4069e3c3f1c93de0b27212a5799ce1e1e98",
      aggregatorUrl: "https://seal-aggregator-testnet.mystenlabs.com",
      weight: 1,
    },
    // Independent server from https://seal-docs.wal.app/Pricing#verified-independent-server-type-key-servers
    {
      objectId: "0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75",
      weight: 1,
    }
  ],
  verifyKeyServers: false,
});

The serverConfigs is a list of objects, where each object contains:

Set verifyKeyServers to true if the app or user needs to confirm that the provided URLs correctly correspond to the claimed key servers, as described in the preceding section. Enabling verification introduces additional round-trip requests to the key servers. For best performance, use this option primarily when verifying key servers at app startup. Set verifyKeyServers to false when verification is not required.

:::tip

API key authentication

Some servers require API keys for authentication:

Include the fields apiKeyName and apiKey in the server configuration object when required. The SDK sends these as an HTTP header in the format apiKeyName: apiKey.

For example, if a server expects the header x-api-key: my123api456key, configure the server object as follows:

{
  objectId: id,
  weight: 1,
  apiKeyName: "x-api-key",
  apiKey: "my123api456key",
  // aggregatorUrl: "...", // Include for decentralized servers
}

Different servers can use different header names. Confirm the header name and API key value with each provider before including it in your threshold configuration.

:::

Encryption

The app can call the encrypt method on the client instance. This function requires the following parameters:

The encrypt function returns two values: the encrypted object, and the symmetric key used for encryption (that is, the key from the DEM component of the KEM/DEM scheme). The symmetric key can either be ignored or returned to the user as a backup for disaster recovery. If retained, the user can decrypt the data manually using the CLI and the symmetric-decrypt command.

const { encryptedObject: encryptedBytes, key: backupKey } = await client.encrypt({
    threshold: 2,
    packageId,
    id,
    data,
});

The encryption does not conceal the size of the message. If message size is considered sensitive, pad the message with zeros until its length no longer reveals meaningful information.

:::note

Seal supports encrypting an ephemeral symmetric key, which you can use to encrypt your actual content. This approach is useful when storing encrypted data immutably on Walrus while keeping the encrypted key separately on Sui. By managing the key separately, you can update access policies or rotate key servers without modifying the stored content.

:::

:::tip

The encryptedBytes returned from the encryption call can be parsed using EncryptedObject.parse(encryptedBytes). It returns an EncryptedObject instance that includes metadata such as the ID and other associated fields.

:::

Decryption

Decryption involves a few additional steps:

Once initialized, the session key can be used to retrieve multiple decryption keys for the specified package without requiring further user confirmation.

const sessionKey = await SessionKey.create({
    address: suiAddress,
    packageId,
    ttlMin: 10, // TTL of 10 minutes
    suiClient: new SuiClient({ url: getFullnodeUrl('testnet') }),
});
const message = sessionKey.getPersonalMessage();
const { signature } = await keypair.signPersonalMessage(message); // User confirms in wallet
sessionKey.setPersonalMessageSignature(signature); // Initialization complete

:::note[Notes on Session Key]

  1. You can also optionally initialize a SessionKey with a passed in Signer in the constructor. This is useful for classes that extend Signer, for example, EnokiSigner.
  2. You can optionally set an mvrName value in the SessionKey. This should be the Move Package Registry name for the package. Seal requires the MVR name to be registered to the first version of the package for this to work. If this is set, the message shown to the user in the wallet would use the much more readable MVR package name instead of packageId.
  3. You can optionally store the SessionKey object in IndexedDB instead of localStorage if you would like to persist the SessionKey across tabs. See usage for import and export methods in the SessionKey class.

:::

The simplest way to perform decryption is to call the client’s decrypt function. This function expects a Transaction object that invokes the relevant seal_approve* functions. The transaction must meet the following requirements:

Seal evaluates the transaction as if the user sent it. In Move, TxContext::sender() returns the account that signed with the session key.

The Seal policy is always checked against the current onchain state of the objects: the key server ignores the version and digest of object inputs in the PTB and resolves each to its latest onchain version.

:::tip

To debug a transaction, call dryRunTransactionBlock directly with the transaction block.

:::

The SealClient caches keys retrieved from Seal key servers to optimize performance during subsequent decryptions, especially when the same ID is used across multiple encryptions. Reusing the same client instance helps reduce backend calls and improve latency. Refer to overall Performance Recommendations.

To retrieve multiple keys more efficiently, use the fetchKeys function with a multi-command PTB. This approach is recommended when multiple keys are required, as it reduces the number of requests to the key servers. Because key servers can apply rate limiting, developers should design their applications and access policies to minimize the frequency of key retrieval requests.

await client.fetchKeys({
    ids: [id1, id2],
    txBytes: txBytesWithTwoSealApproveCalls,
    sessionKey,
    threshold: 2,
});

Check out the integration tests for a full end-to-end example. You can also explore the example app to see how to implement allowlist and NFT-gated content access in practice.

:::tip

If a key server request fails with an InvalidParameter error, the cause can be a recently created onchain object in the PTB input. The key server’s full node might not have indexed it yet. Wait a few seconds and retry the request, as subsequent attempts should succeed once the node is in sync.

:::

Onchain decryption

Seal supports onchain HMAC-CTR decryption in Move through the seal::bf_hmac_encryption package. This enables Move packages to decrypt Seal-encrypted objects and use the results in onchain logic such as auctions, secure voting (see voting.move), or other verifiable workflows.

Use one of the published Seal package IDs as the SEAL_PACKAGE_ID:

To decrypt an encrypted object in a Move package, follow these steps:

Onchain decryption with the TypeScript SDK

You can use the TypeScript SDK to build a transaction that calls Seal’s onchain decryption functions.

Before you decrypt (see Decryption), gather the following:

// 2. Get derived keys from key servers for the encrypted object’s ID. const derivedKeys = await client.getDerivedKeys({ id: encryptedObject.id, txBytes, sessionKey, threshold: encryptedObject.threshold, });

// 3. Get the public keys corresponding to the derived keys. // In practice, this should be done only during the app initialization. const publicKeys = await client.getPublicKeys(encryptedObject.services.map(([service, _]) => service)); const correspondingPublicKeys = derivedKeys.keys().map((objectId) => { const index = encryptedObject.services.findIndex(([s, _]) => s === objectId); return tx.moveCall({ target: ${seal_package_id}::bf_hmac_encryption::new_public_key, arguments: [ tx.pure.address(objectId), tx.pure.vector(“u8”, publicKeys[index].toBytes()) ], }); }).toArray();

// 4. Convert the derived keys to G1 elements. const derivedKeysAsG1Elements = Array.from(derivedKeys).map(([_, value]) => tx.moveCall({ target: 0x2::bls12381::g1_from_bytes, arguments: [ tx.pure.vector(“u8”, fromHex(value.toString())) ], }) );

// 5. Call the Move function verify_derived_keys. This should be cached if decryption for the same ID is performed again. const verifiedDerivedKeys = tx.moveCall({ target: ${seal_package_id}::bf_hmac_encryption::verify_derived_keys, arguments: [ tx.makeMoveVec({ elements: derivedKeysAsG1Elements, type: ‘0x2::group_ops::Element<0x2::bls12381::G1>’ }), tx.pure.address(encryptedObject.packageId), tx.pure.vector(“u8”, fromHex(encryptedObject.id)), tx.makeMoveVec({ elements: correspondingPublicKeys, type: ${SEAL_PACKAGE_ID}::bf_hmac_encryption::PublicKey }), ], });

// 6. Construct the parsed encrypted object onchain. const parsedEncryptedObject = tx.moveCall({ target: ${seal_package_id}::bf_hmac_encryption::parse_encrypted_object, arguments: [tx.pure.vector(“u8”, encryptedBytes)], });

// 7. Construct a list of public key objects. const allPublicKeys = publicKeys.map((publicKey, i) => tx.moveCall({ target: ${seal_package_id}::bf_hmac_encryption::new_public_key, arguments: [ tx.pure.address(encryptedObject.services[i][0]), tx.pure.vector(“u8”, publicKey.toBytes()) ], }));

// 8. Perform decryption with verified derived keys. const decrypted = tx.moveCall({ target: ${seal_package_id}::bf_hmac_encryption::decrypt, arguments: [ parsedEncryptedObject, verifiedDerivedKeys, tx.makeMoveVec({ elements: allPublicKeys, type: ${SEAL_PACKAGE_ID}::bf_hmac_encryption::PublicKey }), ], });

// The decryption result is in an option to be consumed if successful, none otherwise. ```

Optimizing performance

To reduce latency and improve efficiency when using the Seal SDK, apply the following strategies based on your use case:

Other performance recommendations

Choose AES for speed, and reserve HMAC-CTR for onchain decryptions

Use AES for most app data. It is significantly faster and more memory-efficient than HMAC-CTR. Use HMAC_CTR only when you need onchain decryption of small-sized data.

Use envelope (layered) encryption for large payloads

For big files (videos, large datasets), treat Seal as a KMS:

:::tip

Hardware, runtime (browser vs. Node.js), and object size vary. Try both direct AES and envelope encryption to find the best balance of performance, scalability, and manageability for your workload.

:::

Envelope encryption is also recommended for highly sensitive data and enables safer key rotation and updates without re-encrypting large blobs. See Use layered encryption for critical or large data.