Sharing access to encrypted blobs
Two granularities — store-level (the viewer can decrypt every blob in
your BlobStore) and per-blob (only one specific blob keyed by SEAL
marker). Both gates run inside the Move predicate seal_approve that
the SEAL key servers dry-run before releasing the decryption key.
The two ACLs
viewers)Single allowlist on the BlobStore. Members can decrypt every blob you'll ever store — past + future. Best for trusted partners.
per_blob_shares)Table keyed by SEAL marker. One marker → list of allowed viewers. Granting access to one blob doesn't leak the rest of the store.
Both ACLs are owner-managed. Only the BlobStore owner can call
add_* / remove_* Move functions.
Reading the ACL
The SDK exposes the store-level read side:
The per-blob ACL isn't surfaced via a dedicated method yet. Read the
BlobStore object's per_blob_shares field directly via Sui RPC if you need
it.
Writing the ACL
The SDK doesn't build the write-side PTBs for you — but it exports the
PACKAGE_ID and SHARED_OBJECTS so building them is short:
Add a store-level viewer
Add a per-blob share
sealMarker is the 16-byte hex from UploadBlobResult.sealMarker (returned
when you uploaded the blob). For older blobs, it's also stored on the
BlobRef returned by client.blob.list.
Bundle viewers into the upload tx
For the most common pattern (grant initial access at upload time), use the
initialShareViewers array — the SDK bundles add_blob_share calls into
the same PTB as register_blob:
Atomic: either registration + all shares land, or nothing does. Saves a second signature.
Removing access
Symmetric remove_* calls:
SEAL gates access to the key, not the bytes after. A viewer who already pulled the key from the key servers can keep decrypting any blob whose ciphertext they've kept. For true revocation, re-encrypt under a fresh identity and upload a new blob — old ciphertext is then orphaned.
Self-share is rejected
The contract aborts when an address tries to add itself to its own
viewer list (ECannotAddSelf). The SDK's initialShareViewers filters
out the sender's own address client-side to keep the rest of the share
calls successful even if you accidentally include yourself.
The Move predicate
For reference — this is what SEAL's key servers dry-run before releasing a key:
The TypeScript client.blob.canView mirrors the first two checks.
Per-blob lookups aren't in canView because the SDK doesn't load the
per_blob_shares table by default (it's expensive on a store with many
shares).
A simple sharing UX
For a sharing UI:
- Render the store-level viewers list (
client.blob.listViewers) - Form input for a new Sui address
- On submit, build the
add_viewerPTB and sign - Optimistically prepend the new address to the list, reconcile on tx success
For per-blob:
- List the user's encrypted blobs (
client.blob.list, filter byencrypted === true) - For each, show a "Share" button → modal with address input
- Read the BlobRef's
sealMarker, buildadd_blob_share, sign