fingerprintData Privacy & Access

How Onchain Labs protect confidential research data through client-side encryption, on-chain access verification, and a condition-gated key-release flow.

Why Privacy Matters

Scientific research data is often commercially sensitive, personally identifiable, or competitively valuable. Releasing raw experimental results, proprietary compounds, or patient-derived datasets without control can compromise patent applications, regulatory submissions, and competitive advantage. At the same time, the transparency benefits of on-chain science β€” provenance, reproducibility, collaboration β€” require that data exists in a verifiable, shared infrastructure.

Onchain Labs resolve this tension by encrypting data before it enters the public infrastructure. The blockchain records that data exists, who uploaded it, and who can access it β€” but never the data itself. The underlying content is encrypted client-side, stored as ciphertext, and only decrypted inside an authorised client after access conditions have been verified against live on-chain state.

Onchain-Verified Envelope Encryption

Molecule uses Onchain-Verified Envelope Encryption for every confidential file in an Onchain Lab.

  • Client-side encryption. Files are AES-256-GCM encrypted inside the client (browser or AI agent) before they leave the device, using a fresh per-file Data Encryption Key (DEK).

  • Decentralised storage. Ciphertext is pinned to IPFS and persisted to Arweave; access conditions and encryption metadata live alongside the file's provenance record on Kamu (ODF) nodes β€” decentralised by design.

  • On-chain verification. Every decryption is gated by a live on-chain check: the stored access conditions are re-evaluated against current chain state (AccessResolver, IPNFT.canRead, token balances) before the DEK is released. There is no cached permission list.

  • Evolving key custody. The DEK is wrapped by a protocol-operated key custodian today. Custody moves to a BLS threshold operator network (roadmap) without changes to clients, stored metadata, or the on-chain interface.

Files marked as Public skip encryption entirely. The researcher explicitly chooses to make this data openly accessible. Public files still benefit from content addressing, versioning, and provenance tracking, but they carry no confidentiality guarantees by design.

Upload Flow

1. Client β†’ AppSync: initiateCreateOrUpdateFileV2(ipnftUid, contentType,
                                                  contentLength, encryption: true)
2. Backend: authenticate caller (Privy JWT or service token + role check)
3. Backend: issue a fresh per-file DEK β†’
            returns { plaintextDEK (one-shot), wrappedDEK, encryptionSystem }
4. Backend: zero its copy of the plaintextDEK after the response is built
5. Client: AES-256-GCM encrypt(file, plaintextDEK) via SubtleCrypto
6. Client: PUT ciphertext to presigned S3 URL
7. Client: build accessControlConditions (createAccessCondition helper)
8. Client β†’ AppSync: finishCreateOrUpdateFileV2(ipnftUid, uploadToken,
                     encryptionMetadata: { encryptionSystem, wrappedDEK,
                                           iv, contentHash, accessControlConditions,
                                           encryptedBy, encryptedAt })
9. Client: wipe plaintextDEK from memory

The client only opts in to encryption (encryption: true). The backend decides which encryption system to use and returns it in encryptionSystem β€” clients must echo this value verbatim, never hardcode it. This keeps the roadmap upgrade to BLS threshold key custody transparent to existing integrations.

Access Conditions

Who may decrypt a file is determined by on-chain conditions, not by a centralised permission list. When a file is uploaded, the Client SDK attaches an accessControlConditions array to the encryption metadata, stored on Kamu (ODF) alongside the file's provenance record. Conditions are stored but not evaluated at encrypt time β€” they're evaluated at decrypt time against live chain state.

Conditions resolve through the AccessResolver contract, which exposes three principal predicates:

  • Public β€” no condition; anyone can decrypt.

  • Token-Holder β€” isAuthorizedSignerForIpnft(signer, ipnftId) / isAuthorizedSignerForTba(signer, account): passes for IP-NFT holders and any authorized signer resolved recursively through Safe multisigs, Ownable contracts, and ERC-6551 TBAs.

  • Role-gated β€” hasRole(oclId, signer, ROLE_VIEWER | ROLE_CONTRIBUTOR): passes for accounts with an active (non-expired) role grant for the lab. See Roles & Permissions for the full role model and grant lifecycle.

Conditions compose via boolean operators β€” a Lab can, for example, require both Contributor role AND a license NFT before granting access. Future releases will introduce additional composable conditions: credential-gating by minimum IPT holdings, access-list gating by specific wallet addresses, payment-gated unlocks, license-gated access via time-bound license NFTs (ERC-4907), and time-locked conditions that auto-release data at a specified date or block.

Condition Shape

Each entry in accessControlConditions is one of three TypeScript shapes β€” EvmContractCondition for arbitrary view calls, EvmBasicCondition for standard ERC reads, and BooleanCondition as a separator between predicates:

The placeholder :userAddress inside functionParams is substituted with the authenticated caller's wallet at evaluate time. For boolean predicates (hasRole, isAuthorizedSigner*) the returnValueTest is the literal { key: "", comparator: "=", value: "true" }. The full array is JSON-stringified into encryptionMetadata.accessControlConditions β€” typed as String! in the GraphQL schema and parsed back into an array on the backend.

Worked Example: Encrypt for Owner OR Contributor OR Viewer

To encrypt a file so the LabNFT owner, any active Contributor, and any active Viewer can all decrypt it, target the AccessResolver deployment on the chain whose RPC the backend evaluator uses. Substitute <accessresolver-address> below with the right deployment for that chain β€” see the deployments table β€” and use "chain": "base" (canonical), "ethereum", or "sepolia" to match.

hasRole already collapses the role hierarchy on the canonical chain β€” the LabNFT owner passes the admin path inside the contract, a Contributor passes because ROLE_CONTRIBUTOR β‰₯ ROLE_VIEWER, and a Viewer passes directly. So when conditions are evaluated against Base a single condition is enough:

The first functionParams entry is the lab's oclId β€” a packed bytes32 of 0x01 (version) β€– 0x01 (EVM namespace) β€– 10-byte big-endian tokenId β€– 20-byte TBA address. See Onchain Lab for how this identifier is derived. The third entry, "1", is ROLE_VIEWER β€” Contributor and Owner pass the same check thanks to hierarchy.

The explicit OR-composite form is recommended as the cross-chain-safe default. The contract's owner-check (_isLabOwner) returns false off the canonical chain (Mainnet / Sepolia) because the OCL TBA's owner() returns address(0) there, so the role-only condition above will not cover the LabNFT owner if conditions are ever evaluated against a non-Base RPC. OR'ing in isAuthorizedSignerForTba keeps the Owner branch explicit:

The TBA address (<40hex-tba>) is the lower 20 bytes of oclId; tokenId is 10 bytes big-endian sitting between the version/namespace prefix and the TBA. Substitute the AccessResolver address from the deployments table when targeting a non-Base chain. To restrict access to Contributors-and-up only (excluding Viewers), pass "2" (ROLE_CONTRIBUTOR) instead of "1" for the role parameter.

How Conditions Are Evaluated

At decrypt time the backend walks the array left-to-right: each EvmContractCondition is dispatched as a viem readContract call against the configured RPC, the result is compared to returnValueTest, and BooleanCondition separators short-circuit the chain (and stops at the first false, or stops at the first true). Any RPC error fails closed β€” the DEK is not released. A hasRole call whose oclId does not match the lab's canonical binding reverts with InvalidOclId inside the contract and is treated as "condition not met".

Decryption Flow

Decryption is condition-authoritative: the backend reads the stored conditions from their immutable source (Kamu for data-room files, the IPNFT's on-chain tokenURI for agreements), verifies them against live chain state, and only then releases the plaintext DEK. A compromised client cannot substitute weaker conditions.

The GraphQL interface:

For IPFS-pinned agreement files (immutable once minted), the client passes tokenUri (the IPNFT's on-chain tokenURI) and agreementUrl β€” the backend fetches the IPNFT JSON, locates the matching agreement in properties.agreements[], and extracts its encryption block. Because the tokenURI is on-chain, conditions cannot be tampered with after minting.

Agentic Encryption

AI agents encrypt and decrypt lab files through the same GraphQL interface, using a service-token auth path instead of a user Privy JWT:

  • Auth β€” The agent authenticates with an X-Service-Token JWT. For short-lived access, the x402 Gateway mints a per-request token scoped to one mutation after verifying a USDC payment. For long-lived agents, the Molecule team provisions a service token tied to a wallet and an allowedMutations list.

  • Role grant β€” The Lab owner grants the agent's wallet a Contributor (or Viewer) role via AccessResolver.grantRole with isAgent = true and a bounded expiry. The isAgent flag is surfaced in the team-members UI so agent session keys are clearly distinguished from human collaborators.

  • Encrypt β€” The agent calls initiateCreateOrUpdateFileV2(encryption: true), receives a plaintext DEK, encrypts the file locally (Node.js crypto / Web Crypto), uploads the ciphertext, then calls finishCreateOrUpdateFileV2 with the encryption metadata.

  • Decrypt β€” The agent calls decryptDataKey(ipnftUid, filePath). The backend evaluates the stored conditions against live chain state; a valid Viewer/Contributor grant satisfies the hasRole predicate. The backend returns the plaintext DEK over TLS; the agent decrypts locally.

  • Expiry β€” When the role grant expires (block.timestamp >= expiry), hasRole returns false and decryptDataKey starts failing with a conditions-not-met error. The agent must request a fresh grant β€” typically from an owner-controlled orchestrator β€” before it can continue.

See the Developers / AI Agents guide for end-to-end agent integration patterns and the MCP Tools reference for the read-side agent toolset.

Privacy Summary

The net result of this architecture is that no single party has unilateral access to confidential research data. The file content is encrypted before it leaves the client, transmitted as ciphertext, stored as ciphertext across IPFS, Arweave, and S3, and only ever decrypted inside an authorised client after access conditions have been re-verified against live on-chain state. Every action against the data β€” uploads, version changes, access events β€” is recorded with the author's decentralised identifier, creating a tamper-evident provenance trail.

Layer
Protection
Mechanism

At rest

File content encrypted before leaving client

Client-side AES-256-GCM with a per-file wrapped DEK

In transit

All communications over HTTPS; payload is ciphertext

TLS + pre-encryption

Key storage

Plaintext DEK is never persisted; the wrapped DEK is useless without the custodian

Protocol-operated key custodian today; BLS threshold operator network on roadmap

Access control

Decryption gated by a live on-chain verification of stored conditions

AccessResolver (hasRole, isAuthorizedSigner*), IPNFT.canRead

During decryption

Plaintext DEK only exists inside the authorised client for the session

Client-side key assembly and decryption; backend zeroes its copy

Provenance

Every file action tracked with author's DID

Kamu (ODF) version records with did:ethr:{wallet_address}

Permanence

Encrypted content persists even if file record is removed

IPFS + Arweave store ciphertext; keys are separate

Roadmap

Key custody evolves from a single protocol-operated custodian to a BLS threshold operator network. In the target design the DEK is split across the operator set using threshold cryptography, so no single party β€” including Molecule β€” can unwrap it alone. Clients, stored metadata shape, and the on-chain interface stay the same; the encryptionSystem value on new files rolls forward to indicate threshold custody, and the decryptDataKey flow continues to work transparently.

Legacy: Lit Protocol (deprecated for new files)

Lit Protocol is retained read-only for files encrypted before the migration to Onchain-Verified Envelope Encryption. New uploads go through the current flow; the backend no longer generates Lit-encrypted files. Existing Lit-encrypted files continue to decrypt through the Lit SDK until they are migrated. Calling decryptDataKey on a Lit-encrypted file returns an error directing the caller to the Lit SDK.

The legacy model uses Lit Protocol's threshold-cryptography network: a symmetric key is generated client-side, sharded across Lit's decentralised nodes, and only reassembled on the client when a threshold of nodes independently verifies the same on-chain conditions exposed through AccessResolver. Files carry a distinct metadata shape (dataToEncryptHash, litSdkVersion, litNetwork, templateName, contractVersion) and are discriminated by the absence of the encryptionSystem field.

Integrators with Lit-encrypted files in their data rooms should continue using @moleculexyz/storage's Lit flow for those files, and the current flow for everything new. Re-encryption tooling to migrate legacy files is tracked separately.

Last updated