How to store signed and encrypted data on IPFS

How to store signed and encrypted data on IPFS

Storing authenticated and encrypted data on IPFS is a core building block for many Web3 applications, but to date there has been no standardized way to encode this type of data.

Without standards, many developers are forced to create custom formats for their signed and encrypted data. This has hindered the openness and interoperability of information stored in IPFS by storing data in a specific implementation of IPFS. Another way to verify data is to put the data in IPFS and put the CID of the data in a smart contract on a blockchain such as Ethereum. Essentially, this is an expensive way to add a signature on top of the data and persist the signature record on the blockchain.

With the introduction of EIP-2844, a standard that allows wallets to support some new ways to sign and decrypt data based on DIDs and the dag-joseIPLD codec, we can now simply put authenticated and encrypted data directly into IPFS.

What is DID and JOSE?

DID is a W3C standard for decentralized identifiers.

For more details, please refer to our previous article: Astral builds a new world. This article only briefly introduces that DID specifies a general method from a string identifier (such as did:3:bafy...) to a DID document containing a public key for signature verification and key exchange. In most DID methods, the document can be updated when the key is rotated for security reasons.

JOSE is an IETF (Internet Engineering Task Force) standard that stands for JSON Object Signature and Encryption, which pretty much explains what it means . The standard has two main primitives: JWS (JSON Web Signature) and JWE (JSON Web Encryption). Both formats allow for multiple participants: in JWS, there can be one or more signatures on the payload, while in JWE, there can be one or more recipients of the encrypted plaintext.

Building with dag-jose and EIP2844

As we built Ceramic using dag-jose and EIP-2844 as basic building blocks, we created some low-level tooling that allows us to use these technologies more easily.

js-3id-did-provider is an implementation of EIP-2844 using 3ID as a DID method. It can be used as a DID provider on its own or more conveniently in the 3ID Connect library. 3ID Connect allows users to access the DID Provider using their Ethereum wallet (support for more blockchains is coming soon).

key-did-provider-ed25519 is an implementation of EIP-2844 using the Key DID method. It is the simplest DID provider that supports both signing and encryption.

js-did is a library that allows developers to represent users in the form of DIDs. This is the main interface we will be looking at in this tutorial. It enables us to sign data with the currently authenticated user, encrypt data to any user (DID), and decrypt data with the currently authenticated user.

Signature data in IPFS

By using the dag-jose IPLD codec we can create linked and signed data structures. This is done by creating a JSON Web Signature (JWS) that contains links to other data. One of the main issues that the dag-jose codec solves is that the payload of a JWS is traditionally encoded as: base64url This means that if there are IPLD links in it, you will not be able to traverse those links.

Instead, what we do with DagJWS is to coerce the payload into the bytes of a CID. The codec then converts the payload into a CID instance and sets it as a link property of DagJWS. This allows us to easily traverse the resulting DAG.

Setting up IPFS with dag-jose support

Since dag-jose is a new IPLD codec, it is not yet included in js-ipfs by default. It also implements the new IPLD codec API, which js-ipfs does not support yet. Therefore, when creating an IPFS instance, you need to do the following:

import IPFS from 'ipfs' import dagJose from 'dag-jose' import multiformats from 'multiformats/basics' import legacy from 'multiformats/legacy' multiformats.multicodec.add(dagJose)const dagJoseFormat = legacy(multiformats, dagJose.name)const ipfs = await Ipfs.create({ ipld: { formats: [dagJoseFormat] } })

Make sure you install the correct multi-format version:

$ npm i [email protected]
Setting up a DID instance

In the example setup below, we use key-did-provider-ed25519. If you choose to use the network from above, 3ID Connect and js-3id-did-provider will be used behind the scenes.

import { DID } from 'dids'import { Ed25519Provider } from 'key-did-provider-ed25519'import KeyResolver from '@ceramicnetwork/key-did-resolver'import { randomBytes } from '@stablelib/random'// generate a seed, used as a secret for the DIDconst seed = randomBytes(32)// create did instanceconst provider = new Ed25519Provider(seed)const did = new DID({ provider, resolver: KeyResolver.getResolver() })await did.authenticate()window.did = didconsole.log('Connected with DID:', did.id)
Create a signed data structure

Now we can start signing and adding data to IPFS! First, let's create a simple function that takes a payload, signs it using the did.createDagJWS method, and then adds the resulting data to IPFS.

As we can see in the code below, we get two objects from this method: jwsDagJWS itself and the raw bytes of the linkedBlock encoded payload. What happens in the background is that the payload is encoded using dag-cbor, after which the CID of the encoded payload is used as the payload created by jws. We can access the payload CID on the DagJWS instance through jws.link.

async function addSignedObject(payload) { // sign the payload as dag-jose const { jws, linkedBlock } = await did.createDagJWS(payload) // put the JWS into the ipfs dag const jwsCid = await ipfs.dag.put(jws, { format: 'dag-jose', hashAlg: 'sha2-256' }) // put the payload into the ipfs dag await ipfs.block.put(linkedBlock, { cid: jws.link }) return jwsCid}

Using this function, let's create our first signed data object:

// Create our first signed objectconst cid1 = await addSignedObject({ hello: 'world' })// Log the DagJWS:console.log((await ipfs.dag.get(cid1)).value)// > {// > payload: "AXESIHhRlyKdyLsRUpRdpY4jSPfiee7e0GzCynNtDoeYWLUB",// > signatures: [{// > signature: "h7bHmTaBGza_QlFRI9LBfgB3Nw0m7hLzwMm4nLvcR3n9sHKRoCrY0soWnDbmuG7jfVgx4rYkjJohDuMNgbTpEQ",// > protected: "eyJraWQiOiJkaWQ6MzpiYWdjcWNlcmFza3hxeng0N2l2b2tqcW9md295dXliMjN0aWFlcGRyYXpxNXJsem 4yaHg3a215YWN6d29hP3ZlcnNpb24taWQ9MCNrV01YTU1xazVXc290UW0iLCJhbGciOiJFUzI1NksifQ"// > }],// > link: CID(bafyreidykglsfhoixmivffc5uwhcgshx4j465xwqntbmu43nb2dzqwfvae)// > }// Log the payload:ipfs.dag.get(cid1, { path: '/link' }).then(b => console.log(b.value))// > { hello: 'world' }// Create another signed object that links to the previous oneconst cid2 = addSignedObject({ hello: 'getting the hang of this', prev: cid1 })// Log the new payload:ipfs.dag.get(cid2, { path: '/link' }).then(b => console.log(b.value))// > {// > hello: 'getting the hang of this'// > prev: CID(bagcqcerappi42sb4uyrjkhhakqvkiaibkl4pfnwpyt53xkmsbkns4y33ljzq)// > }// Log the old payload:ipfs.dag.get(cid2, { path: '/link/prev/link' }).then(b => console.log(b.value))// > { hello: 'world' }

Note that since the payload will be signed by your DID, the values ​​for CID and JWS will be different for you.

Data structure for verifying signature

Verifying a JWS is very simple. Simply retrieve the JWS object and pass it to the verifyJWS method. If the signature is invalid, this function will throw an error. If the signature is valid, it will return the DID (with the key fragment) used to sign the JWS.

const jws1 = await ipfs.dag.get(cid1)const jws2 = await ipfs.dag.get(cid2)const signingDID1 = await did.verifyJWS(jws1)await did.verifyJWS(jws2)
Encrypted Data in IPFS

Signing data in IPFS is a difficult problem, but perhaps more interesting is encrypting data. By using dag-jose and EIP-2844, we can encrypt data into one or more DIDs and store it directly in IPFS. Below, we demonstrate how to do this using the convenient tools provided by the js-did library.

Encrypting IPLD Data

There is a simple method to create a DagJWE object that is encrypted to one or more DIDs createDagJWE. This method accepts an IPLD object (which may also include a JSON object of CID links) and an array of DIDs.

It will parse the DID to retrieve the public encryption keys found in its DID document and create a JWE encrypted to those keys. First, let's create a helper function that creates a JWE and puts it into IPFS.

async function addEncryptedObject(cleartext, dids) { const jwe = await did.createDagJWE(cleartext, dids) return ipfs.dag.put(jwe, { format: 'dag-jose', hashAlg: 'sha2-256' })}

Once we have this functionality we can create some encrypted objects. In the following example we first create a simple encrypted object and then create an additional encrypted object that is linked to the previous object.

const cid3 = await addEncryptedObject({ hello: 'secret' }, [did.id]) const cid4 = await addEncryptedObject({ hello: 'cool!', prev: cid3 }, [did.id])

Note that in the example above, we used [did.id]( ) to encrypt the data to the currently authenticated DID. We can of course also encrypt the data to a DID of a user that is not locally authenticated (e.g. another user)!

Decrypting IPLD Data

After retrieving the data from IPFS, we will only get the encrypted JWE. This means we need to decrypt the data after getting it. Since we have already created objects that are linked to each other, let's create a function to retrieve these objects and recursively decrypt them.

async function followSecretPath(cid) { const jwe = (await ipfs.dag.get(cid)).value const cleartext = await did.decryptDagJWE(jwe) console.log(cleartext) if (cleartext.prev) { followSecretPath(cleartext.prev) }}

The function above is an example that simply logs the decrypted objects. We can use it to view the contents of these objects.

// Retrieve a single objectfollowSecretPath(cid3)// > { hello: 'secret' }// Retrive multiple linked objectsfollowSecretPath(cid4)// > { hello: 'cool!', path: CID(bagcqceraqittnizulygv6qldqgezp3siy2o5vpg66n7wms3vhffvyc7pu7ba) }// > { hello: 'secret' }

<<:  Ethereum network's daily transaction volume is 28% higher than Bitcoin's

>>:  What is the value of Polkadot ecosystem in 2021? | Online Classroom

Recommend

What does it mean for a person who will achieve success in the future?

People's ideas have changed a lot now. There ...

Current state of Bitcoin mining in Q2 2024

Key Takeaways: After the halving, Bitcoin’s hash ...

How to read a man's fortune

Everyone has a destiny when they are born, and ev...

The abdomen shows the position in life

The abdomen shows the position in life Physiognom...

How to interpret the face of a miserable woman?

Everyone's destiny is different, but people w...

Is the face of a person with willow-leaf eyes good?

When it comes to a person's face, whether it ...

A high nose indicates prosperity of wealth

The nose is the most prominent part of a person&#...

Argo Blockchain CEO Becomes First Public Company Executive to Be Paid in Bitcoin

On Wednesday, British cryptocurrency mining compa...

The fate of people with a bulge on the back of their heads

There is one part that many people tend to overlo...

Palmistry to analyze whether he is rich

Palmistry to analyze whether he is rich Ming Dyna...

Short nose woman

Some people have short noses, some have long nose...

Observing Cryptocurrency Mining from a Statistical Perspective

Author: Luxor Tech Translation: Zoe Zhou Source: ...