Skip to content

Access Control Contract

OpenTusk shared vaults use an on-chain Sui Move contract to enforce cryptographic access control. The contract manages Whitelists — shared objects on the Sui blockchain that list which Sui addresses can decrypt files in a given vault.

The opentusk::shared_vault contract is deployed on Sui mainnet:

0xbb3f0933b07bb20a96129e05304905ba049db4a8f1c6005da1f5871c28367717

View it on the Sui Explorer.

When you create a shared vault, OpenTusk deploys two on-chain objects:

ObjectTypePurpose
WhitelistShared objectLists authorized Sui addresses. SEAL key servers read this to verify decryption access.
CapOwned objectAdmin capability for adding/removing members. Held by the vault owner’s Sui wallet.
Create shared vault
└─ Vault owner calls create_whitelist_entry()
├─ Whitelist shared object created (stores member addresses)
└─ Cap transferred to vault owner (admin control)
Add member
└─ Vault owner calls add(whitelist, cap, member_address)
└─ Member's Sui address added to on-chain Whitelist
Decrypt file
└─ SEAL key server calls seal_approve(encryption_id, whitelist)
├─ Verifies encryption ID has whitelist ID as prefix
└─ Verifies caller's address is in the Whitelist
└─ If both pass → decryption key fragment issued

The full Move source code for the opentusk::shared_vault module:

Apache-2.0
// Copyright (c) OpenTusk
/// Shared vault access control for the SEAL protocol.
///
/// Each shared vault gets a Whitelist object (from the SEAL whitelist pattern).
/// The vault owner holds a Cap that controls membership.
/// SEAL key servers call seal_approve to verify decryption access.
///
/// Key format: [package_id][whitelist_id][nonce]
/// - Any data encrypted to this key-id can be decrypted by whitelisted members.
/// - The nonce allows per-file key derivation within the same whitelist.
module opentusk::shared_vault;
use sui::table;
const ENoAccess: u64 = 1;
const EInvalidCap: u64 = 2;
const EDuplicate: u64 = 3;
const ENotInWhitelist: u64 = 4;
const EWrongVersion: u64 = 5;
const VERSION: u64 = 1;
/// Per-vault whitelist. Shared object on-chain.
/// Members listed here can decrypt files encrypted to this vault's key-id.
public struct Whitelist has key {
id: UID,
version: u64,
addresses: table::Table<address, bool>,
}
/// Capability granting admin control over a Whitelist.
/// Held by the vault owner (transferred to them at creation).
public struct Cap has key, store {
id: UID,
wl_id: ID,
}
/// Create a new whitelist and return the admin cap.
/// The caller (platform wallet or vault owner) receives the Cap.
public fun create_whitelist(ctx: &mut TxContext): (Cap, Whitelist) {
let wl = Whitelist {
id: object::new(ctx),
version: VERSION,
addresses: table::new(ctx),
};
let cap = Cap {
id: object::new(ctx),
wl_id: object::id(&wl),
};
(cap, wl)
}
/// Share the whitelist so SEAL key servers can read it.
public fun share_whitelist(wl: Whitelist) {
transfer::share_object(wl);
}
/// Entry function: create whitelist, share it, transfer cap to sender.
entry fun create_whitelist_entry(ctx: &mut TxContext) {
let (cap, wl) = create_whitelist(ctx);
share_whitelist(wl);
transfer::public_transfer(cap, ctx.sender());
}
/// Add a member to the whitelist. Requires the matching Cap.
public fun add(wl: &mut Whitelist, cap: &Cap, account: address) {
assert!(cap.wl_id == object::id(wl), EInvalidCap);
assert!(!wl.addresses.contains(account), EDuplicate);
wl.addresses.add(account, true);
}
/// Remove a member from the whitelist. Requires the matching Cap.
public fun remove(wl: &mut Whitelist, cap: &Cap, account: address) {
assert!(cap.wl_id == object::id(wl), EInvalidCap);
assert!(wl.addresses.contains(account), ENotInWhitelist);
wl.addresses.remove(account);
}
/// Check if a caller is authorized to decrypt data encrypted to
/// this whitelist's key-id.
/// Verifies: correct version, id prefix matches whitelist, caller
/// is a member.
fun check_policy(
caller: address,
id: vector<u8>,
wl: &Whitelist,
): bool {
assert!(wl.version == VERSION, EWrongVersion);
// Verify the encryption id has the whitelist id as prefix
let prefix = wl.id.to_bytes();
let mut i = 0;
if (prefix.length() > id.length()) {
return false
};
while (i < prefix.length()) {
if (prefix[i] != id[i]) {
return false
};
i = i + 1;
};
// Check membership
wl.addresses.contains(caller)
}
/// SEAL entry point. Key servers call this to verify decryption access.
entry fun seal_approve(
id: vector<u8>,
wl: &Whitelist,
ctx: &TxContext,
) {
assert!(check_policy(ctx.sender(), id, wl), ENoAccess);
}
FunctionAccessDescription
create_whitelist_entryEntryCreates a Whitelist + Cap, shares the Whitelist, transfers Cap to caller
addPublicAdds a Sui address to a Whitelist (requires matching Cap)
removePublicRemoves a Sui address from a Whitelist (requires matching Cap)
seal_approveEntryCalled by SEAL key servers to verify a caller can decrypt a given encryption ID

SEAL encryption IDs follow the format:

[whitelist_object_id][nonce]
  • whitelist_object_id — the 32-byte Sui object ID of the vault’s Whitelist
  • nonce — a per-file nonce (typically derived from the file ID) enabling unique encryption keys per file within the same vault

The seal_approve function verifies that the encryption ID starts with the Whitelist’s object ID (prefix check) and that the caller is a member of that Whitelist.

OpenTusk runs a self-hosted SEAL key server that validates decryption requests against the on-chain Whitelist:

ParameterValue
Key server object0x6ab907c5cdb1e3abe1f3c2c5ac4c853bf4a5932ae92886c40a22605540128803
Threshold1-of-1
URLhttps://seal.opentusk.ai

The key server calls seal_approve on-chain to verify membership before issuing decryption key fragments.

CodeConstantMeaning
1ENoAccessCaller is not authorized (not in Whitelist or invalid ID prefix)
2EInvalidCapCap does not match the Whitelist
3EDuplicateAddress is already in the Whitelist
4ENotInWhitelistAddress is not in the Whitelist (on removal)
5EWrongVersionWhitelist version mismatch