The most popular method used for sharing files off-chain in Web3 is IPFS, and there are some good reasons for that. However it does not come without its own share of problems, and one of those is the ability to share private files. IPFS is a public network so anyone with a CID can access and download that content, and this hinders projects that may want to token gate content or create subscriptions to content. With that said, encryption has proven to be one solution to this problem. Remarkably, the solution of asymmetric encryption is used in blockchain all the time and can be reused for the purpose of token gating. Lit Protocol is a decentralized middleware client that enables access controls to help extend asymmetric encryption to token gating based on crypto ownership, such as owning an NFT, ERC-20 token balance, or simply designating a recipient address. In this post, we’ll show you how you can combine the best of both worlds and create an app that will encrypt content, upload it to IPFS, and then given an encrypted CID, decrypt it.
IPFS is public and openly available, but it’s also not permanent by default. This means that, unlike blockchain storage protocols that make every piece of content permanent as soon as it’s uploaded, you can potentially remove content from IPFS. You’ll see why this is important as we explore encryption more deeply.
The biggest problem with encryption is that its always evolving. One encryption method we use today will be outdated one day in the future. An example is MD5 which was cracked almost 10 years ago but people still use it without knowing the risk. When we consider putting files on a decentralized network that are specifically designed to not be taken down, things get messy. Arweave is a common consideration for encryption and decentralized storage, however, their model puts content on the network permanently. There is the possibility those encrypted files could be cracked in another 10 years.
IPFS is different in that content is not “permanent,” but rather it is “persistent.” It's a subtle difference but has massive ramifications. With IPFS, the content will only stay on the network if at least one IPFS node keeps the content “pinned,” which tells other nodes that might have a cached copy of the content to keep it available. As soon as there are no nodes pinning a particular CID, then the nodes holding that cache will dump it when they use garbage collection. It's a unique mechanism that helps prevent digital waste and ensures only the content we value will persist. The concepts of permanence and persistence are truly philosophical differences of approaching the same problem.
When you combine IPFS with encryption, you get a unique situation where content that is no longer used can be unpinned. Granted it does not guarantee the content will be completely wiped from the network, but it does give users a level of control over their content they would normally not have with other decentralized storage networks. It is also unlikely that bad actors would go through the trouble and costs to keep encrypted content pinned for the purpose of decrypting it years down the road. The cost of storage helps balance situations like these. With that said let's actually build this thing!
Building the App
What’s great about this project is that we already have most of what we need to build it! Pinata created a Next.js template a while back which we can use again and just add in our Lit Protocol SDK.
To follow this tutorial you will want to make sure you have the following:
Before going any further make sure you get a free Pinata account so you can make an API key and get a free Dedicated Gateway for this project! Once you make an API key, save the <span class="code-inline">JWT</span> that we’ll use in a little bit, as well as the gateway domain for your Dedicated Gateways.
Thats it! To kick it off, simply run the command to use the Pinata Next.js Template
Back in the terminal the next thing we’re going to do is install the Lit Protocol SDK. We’ll be using the V3 of the SDK which is in beta, and you can install based on their docs here or use this command:
The last thing you need to do to set up the project is open the <span class="code-inline">.env.sample</span> file which should look like this:
Paste in the <span class="code-inline">PINATA_JWT</span> that you made earlier when you set up your Pinata account and also paste in the <span class="code-inline">NEXT_PUBLIC_GATEWAY_URL</span> with the format <span class="code-inline">https://mygateway.mypinata.cloud</span> with of course your own domain URL. Then change the name of the file from <span class="code-inline">.env.sample</span> to <span class="code-inline">.env.local</span>, a very important step for our app to work!
Now let's go ahead and spin up the dev server with <span class="code-inline">npm run dev</span> and start building. All of our work will be done in just one file, <span class="code-inline">pages/index.tsx</span>; easy! We’ll start by importing the Lit Protocol SDK at the top of the file.
Inside the <span class="code-inline">Home</span> component, we’ll add another state variable that we’ll come back to later.
The great thing about this template is that it's already got uploads to IPFS with Pinata baked in with an <span class="code-inline">/api/files</span> route on the backend, so all we have to do is encrypt the file before we upload it. We’ll do this in the <span class="code-inline">uploadFile</span> function inside of <span class="code-inline">Home</span>, and it should look like this to start.
At the top of our <span class="code-inline">try</span> statement but underneath our <span class="code-inline">setUploading</span> state, we’ll initialize the <span class="code-inline">LitNodeClient</span> using the <span class="code-inline">ceyenne</span> network, connect our app to that network, then get the <span class="code-inline">authSig</span>. Lit Protocol is a decentralized network middleware that helps us do some cool token gating and lets us do encryption. In these few statements, we create a client that connects to that middleware network and then gets a signature from the user. This signature will be used for signing the encrypted files.
Next up we’ll set up our access controls for this encrypted content. We’ll dive deeper into this later in the tutorial, but essentially this is the most important part of our app as it determines who can decrypt the content we encrypt. It could be something like token gating by NFT collection or a direct address. For now, we’ll keep it simple and allow anyone with a balance of 0 ETH or higher to decrypt it (that should be everyone with a wallet).
The fun part; encrypting! There are several methods of encryption that Lit Protocol offers through their SDK, such as just a string, or a file, and in our case, we’ll use the <span class="code-inline">encryptFileAndZipWithMetadata</span> method. This is handy because in order to decrypt a file, our recipient will need the <span class="code-inline">accs</span> parameters we set and a secure hash. We want a simple way for all of this to be packaged and included in our IPFS CID, and that's exactly what this method will do. All we have to do is pass in our access control conditions array, our <span class="code-inline">authSig</span>, the chain, our <span class="code-inline">fileToUpload</span> that we passed into the function argument, the <span class="code-inline">litNodeClient</span>, and finally a simple <span class="code-inline">readme</span> that will explain to whoever happens to download it from IPFS what they need to do with it.
One last little touch we need to do is adapt this encrypted zip file so it will be accepted by our <span class="code-inline">/api/files</span> endpoint, and we’ll do so with just two lines of code.
All together we should have an upload function that looks like this:
One thing to note is that there is a file size restriction when using the Next API routes, so if you have larger files you may want to move uploading to the client side and utilize pre-signed JWTs which we talk about in this post.
We now have encrypted uploads! If you upload a file through the app you should get a CID, and if you download the file it will result in a zip folder with all the stuff we just made. This is cool, but how do we decrypt it? How do we let other people decrypt it? Its actually pretty easy! We’re gonna make a new function right below our upload function called <span class="code-inline">decryptFile()</span>, which will take a <span class="code-inline">fileToDecrypt</span> CID. First thing we’ll do is fetch that file using our Dedicated Gateway and turn it into a blob.
Now we can re-create the <span class="code-inline">litNodeClient</span> and get the auth signature. The beauty of this SDK is that once some signs they will not need to sign again unless they disconnect from the app, making the interactions fairly smooth.
Just like we used <span class="code-inline">encryptFileAndZipWithMetadata</span> method, we have a matching method for decryption called <span class="code-inline">decryptZipFileWithMetadata</span> which we’ll use very similarly to the encryption. The zip folder has everything we need so all we have to pass in is the <span class="code-inline">file</span> blob, our <span class="code-inline">litNodeClient</span>, and the recipient authSig. Piece of cake! From this, we’ll extract the <span class="code-inline">decryptedFile</span> and <span class="code-inline">metadata</span> from our request.
All that’s left to do is deliver the file to the user! There are several ways you could go about it, for example, if you want to display the content to the user such as an image or video you could do so with a bit of formatting. In this example, we’ll just trigger a download to the recipient’s computer. All together we should have the following.
One small little change we’ll make to the JSX is a text input where someone can paste in a CID and a “Decrypt” button someone can press after pasting in their CID.
With all of this together, you should have an app with the following flow!
You can also download and use this exact template here!
This little template is really designed just to get you started and help you understand how Pinata and Lit Protocol work, and there is so much you can do with it. I would highly recommend checking out Lit Protocol’s documentation, in particular their section on all the different access controls you can do. For example, if you only wanted holders of a particular ERC721 NFT you could use the following.
Or you could do DAO membership (MolochDAOv2.1, also supports DAOHaus)
You can even do a simple check if the recipient is a particular wallet address.
With these building blocks, you could easily build a standalone token gating app, or build a custom solution for your holders. The possibilities are endless!