Ceramic + TACo

This guide demonstrates how to integrate TACo with one of Ceramic's data services, ComposeDB. TACo and Ceramic's data services are essential and mutually complementary components of the Web3 stack, offering developers ‘Web 2.0’ functionality without compromising on decentralization.

On completion of this tutorial, it will be possible to:

  1. Specify fine-grained access control logic for encrypted data/streams saved to the ComposeDB graph database.

  2. Have data requesters authenticate themselves with a reused Ceramic sign-in (a Sign-In With Ethereum message) in order to decrypt the retrieved data/stream.

  3. Securely gate-keep any format, size or throughput of data stream, with access collectively managed by a permissionless and customizable group of TACo nodes.

ComposeDB Overview

A data service built on Ceramic, the ComposeDB graph database offers enhanced composability and ease of querying. ComposeDB comes with native GraphQL support and automatically splits read/write load for additional performance. When running a Ceramic node with ComposeDB, developers can define their data models using GraphQL, or choose to begin indexing on existing data models already defined by the community, or both.

ComposeDB can be leveraged in concert with TACo, to ensure that returned data and messages are only decryptable by parties satisfying pre-specified conditions. Interactions with ComposeDB (via the Ceramic network) and TACo (via the Threshold network) can be architected to occur in parallel, which means non-cumulative latency. TACo's Single Sign On framework also provides the basis for extensions to broader user identity and authentication, including compatibility with Ceramic's library of DIDs (e.g. PKH-Ethereum).

Use Cases

  • Social networks & Knowledge Bases. Leverage Ceramic's verifiable credentials and TACo's credential-based decryption to ensure that private user-generated content is only viewable by those who are supposed to see it, and nobody else.

  • IoT event streams. Let sensitive data flow from sensors to legitimate recipients, without trusting an intermediary server to handle the routing and harvest metadata. For example, a medical professional can be issued a temporary access token if a patient's wearable output data rises above a certain threshold.

  • LLM chatbots. Messages to and from a chatbot should be 100% private, not mined by a UX-providing intermediary. Harness Ceramic's web-scale transaction processing and TACo's per-message encryption granularity to provide a smooth and private experience for users of LLM interfaces.

Demo Application

In this section of the tutorial, we will set up a simple browser-based messaging application that makes combining ComposeDB and TACo more intuitive. This involves running a local Ceramic node, to which TACo-encrypted messages are saved and immediately queryable by data requestors.

This demo repo is based on a fork of ceramicstudio’s lit-composedb repo, with the TACo library replacing LIT. This demonstrates how applications that have already integrated LIT’s permissioned service can easily substitute it for TACo’s decentralized access control plugin.

This demo requires:

  • A Metamask wallet with Polygon Amoy testnet added, and multiple accounts to mimic a real-world decryption flow.

  • A positive balance of Polygon Amoy testnet tokens (> 0.00 MATIC) held in one of the accounts, in order to satisfy the default access conditions.

First, we’ll clone the reference repository.

git clone https://github.com/nucypher/taco-composedb

Next, we install the dependencies. This requires node v16 running in our terminal.

npm install 

Next, we’ll generate parameters for the local Ceramic node. This command defines the ComposeDB config file, and generates the admin seed & admin DID credentials.

npm run generate

The commands above only need to be executed once.

Finally, we launch the browser application and Ceramic node. This is the only step required for subsequent runs.

npm run dev

Open the app on http://localhost:3000 and sign in with Ceramic. Note that this signature can be reused to authenticate the data consumer later in the flow, if we maintain the same session. However, we begin as the data producer, and enter a message into the chat box element:

When we click Send, the app will prompt us to sign the message. The TACo API requires a specific signature (i.e. not SIWE) to encrypt data, so that later, TACo nodes are able to validate that (1) we are authorized to encrypt, using that particular group of nodes, and (2) we specified the conditions for decryption. Once we have provided this signature, the message is encrypted locally and we're presented with a ciphertext:

We now switch to the data consumer’s perspective by disconnecting and reconnecting with a new account in Metamask. This mimics the majority of use cases in which a data consumer will make this request from a new device or identity.

Having established a session with a new account, we will need to authenticate ourselves. When we hit the Decrypt button, it prompts us to sign in with Ceramic. Note that this signature is set to expire after 2 hours, during which time we can decrypt as many messages as we like, as the signature will remain cached.

Our authenticated identity – a unique EVM wallet address that we have proven we control – must now satisfy the prespecified conditions. In this example, the qualifying conditions are the wallet holding any amount of MATIC in a Polygon Amoy wallet greater than zero.

Note that in this demo, the end-user is not choosing the conditions for data access via the browser UX. However, developers can modify these default access conditions, or enable the user to specify access conditions directly. This functionality is explored in later sections.

We’ll first attempt to decrypt whilst failing to fulfill the conditions. In this instance, this account contains zero MATIC:

As expected, we are denied access to the plaintext:

We’ll now fund the wallet with MATIC in order to satisfy the requisite conditions. We could also switch accounts to a wallet with the requisite balance.

This time, when we hit the Decrypt button and ping the TACo API, the assigned group of nodes validates our fulfillment of the access conditions – without a need to authenticate again. The wallet now satisfies the required access condition and the nodes will subsequently deliver the decryption fragments required for us to decrypt. We assemble the fragments locally, and can view the original plaintext message:

To clear the session data and restart the demo, click the Reset button in the navigation bar.

Note that this tutorial utilizes the parameters ritualId = 0 and domains.TESTNET. These refer to an permissionless DKG public key and hacker-facing testnet respectively. Although fully functional and up-to-date with Mainnet, this development environment is not decentralized and unsuitable for real-world sensitive data. For more information, see the trust assumptions section.

Having illustrated the basic concepts via the example application, we’ll now look at the underlying code and how to configure it to your use case.

Specifying conditions & authentication

There are two distinct ways in which a data consumer must prove their right to access the private data. The data producer can customize both of these, and combine them in any way they see fit.

  1. Data consumers must authenticate themselves – i.e. prove their identity. In this tutorial, we chose an EVM-based identity. More concretely, we require TACo nodes to process the claim that the requestor has already authenticated themselves within the app they are using, and they did so via Sign In With Ethereum (SIWE).

  2. Data consumers must satisfy a set of conditions – i.e. some public web state that TACo nodes can either validate or invalidate. In the demo above, we simply required requestors to hold testnet MATIC. Here, we’re going to build on this by adding a time-based condition, and then combine them into a ConditionSet.

We’ll begin by specifying the authentication method and the first condition. This is the same configuration as the demo above and can be viewed in the repository here. The method is an RPC function that checks the balance of the data consumer based on the identity they provide, which in this case will be authenticated via a EIP4361 (SIWE) message already utilized by the application. The chain ID refers to Polygon Amoy.

import { conditions } from "@nucypher/taco"; 

const rpcCondition = new conditions.base.rpc.RpcCondition({
    chain: 80002,
    method: 'eth_getBalance',
    parameters: [':userAddressExternalEIP4361'],
    returnValueTest: {
        comparator: '>',
        value: 0,
    },
});

Next, we’ll logically combine this condition with a second condition via a CompoundConditionusing the AND operator, which means both conditions must be satisfied for data access. The second condition allows access only until the end of 2024, via a standard block timestamp.

const timeBox = new conditions.base.time.TimeCondition({
    chain: 11155111,
    returnValueTest: {
        comparator: '<=',
        value: 1735689599,
    },
});

const twoConditions = new conditions.compound.CompoundCondition({
    operator: 'and',
    operands: [rpcCondition, timeBox],
});

Read more about condition types here.

Encrypting & saving the data

Then, we put it all together. We specify the aforementioned testnet domain and ritualId, and also utilize a standard web3 provider/signer. The output of this function is known as a messageKit – a payload containing both the encrypted data and embedded metadata necessary for a qualifying data consumer to access the message.

import { initialize, encrypt, conditions, domains, toHexString } from '@nucypher/taco';
import { ethers } from "ethers";

// We have to initialize the TACo library first
await initialize();

const web3Provider = new ethers.providers.Web3Provider(window.ethereum);
const ritualId = 0
const message = "I cannot trust a centralized access control layer with this message.";

const messageKit = await encrypt(
    web3Provider,
    domains.TESTNET,
    message,
    twoConditions,
    ritualId,
    web3Provider.getSigner()
);
const encryptedMessageHex = toHexString(messageKit.toBytes());

Querying & decrypting the data

We're now going to enable data consumers to access the underlying data, if and only if the two conditions we specified are satisfied. Data consumers interact with the TACo API via the decrypt function, including the following arguments:

  • Provider – Web3 provider to connect to Polygon.

  • Domain – which TACo network (Mainnet, Testnet ).

  • encryptedMessage/ThresholdMessageKit – this contains the encrypted plaintext and the access conditions, supplied to the data consumer via a side channel.

  • conditionContext – this enables on-the-fly, programmatic population of context variable values used within conditions, the most important being data consumer authentication. Developers can predicate certain authentication methods on certain conditions. For example:

    • If conditions are based around EVM state, authenticate via SIWE.

    • (In future versions) If conditions are based around social account ownership, authenticate via OAuth.

import {conditions, decrypt, Domain, encrypt, ThresholdMessageKit} from '@nucypher/taco';
import {ethers} from "ethers";

export async function decryptWithTACo(
    encryptedMessage: ThresholdMessageKit,
    domain: Domain,
    conditionContext?: conditions.context.ConditionContext
): Promise<Uint8Array> {
    const provider = new ethers.providers.Web3Provider(window.ethereum);
    return await decrypt(
        provider,
        domain,
        encryptedMessage,
        conditionContext,
    )
}

In this case, the data consumer must also provide an existing SIWE message signature, to prove their on-chain identity. This is shown below, along with the definition of the function decryptWithTaco:

const mkB64 = message.ciphertext;
const mkBytes = await decodeB64(mkB64);
const thresholdMessageKit = ThresholdMessageKit.fromBytes(mkBytes);

// obtain existing SIWE message and signature from application
const {messageStr, signature} = await getCeramicSiweInfo(currentAddress);

// create corresponding user authentication provider
const singleSignOnEIP4361AuthProvider = await SingleSignOnEIP4361AuthProvider.fromExistingSiweInfo(messageStr, signature);
const conditionContext = conditions.context.ConditionContext.fromMessageKit(thresholdMessageKit);
conditionContext.addAuthProvider(USER_ADDRESS_PARAM_EXTERNAL_EIP4361, singleSignOnEIP4361AuthProvider);

// decrypt the data
decryptedMessageBytes = await decryptWithTACo(
    thresholdMessageKit,
    domains.TESTNET,
    conditionContext,
);

Using ComposeDB & TACo in production

  • For Ceramic, connect to Mainnet (domains.MAINNET).

  • For TACo, a funded Mainnet ritualID is required – this connects the encrypt/decrypt API to a cohort of independently operated nodes, and corresponds to a DKG public key generated by independent parties. A dedicated ritualID for Ceramic + TACo projects will be sponsored soon. Watch for updates here or in the Discord #taco channel.

As noted, the parameters specified in this guide are for testing and hacking only. For real-world use cases where uploaded data should remain private & permanent, the production version of TACo is required.

Last updated