src/content/post/How to Encrypt and Decrypt Files on IPFS Using Lit.md 23.0 K raw
1
---
2
title: "How to Encrypt and Decrypt Files on IPFS Using Lit Protocol"
3
publishDate: "04 Nov 2023"
4
description: "Experience the power of decentralized storage, encryption, and token gating with this tutorial"
5
tags: ["ipfs", "lit-protocol", "encryption", "token-gating"]
6
ogImage: "https://assets-global.website-files.com/629e4fe96456f8219203e7f1/6545bfa112815d6340466066_20231103_How%20to%20Encrypt%20and%20Decrypt%20Files%20on%20IPFS%20Using%20Lit%20Protocol%20and%20Pinata.jpeg"
7
---
8
9
10
The most popular method used for sharing files off-chain in Web3 is IPFS, and there are some [good reasons for that](https://www.pinata.cloud/blog/why-ipfs-is-the-storage-solution-for-web3-developers). 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](https://www.okta.com/identity-101/asymmetric-encryption) is used in blockchain all the time and can be reused for the purpose of token gating. [Lit Protocol](https://litprotocol.com/) 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.
11
12
## Why IPFS?
13
14
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.
15
16
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 perhaps 10 years ago](https://www.okta.com/identity-101/md5/#:~:text=The%20MD5%20hash%20function's%20security,be%20used%20for%20malicious%20purposes.) 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.
17
18
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](https://www.pinata.cloud/blog/what-is-pinning),” 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.
19
20
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](https://www.pinata.cloud/blog/is-ipfs-free) helps balance situations like these. With that said let's actually build this thing!
21
22
<aside>
23
ℹ️ <b>Disclosure:</b> There will always be limitations to encryption and eventually current methods may be cracked. Please be aware of the risk involved and be sure to read <a href="https://developer.litprotocol.com/v3/sdk/authentication/security">Lit Protocol’s best practices for security.</a>
24
</aside>
25
26
## Building the App
27
28
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](https://www.pinata.cloud/blog/announcing-pinata-ipfs-developer-starter-templates) a while back which we can use again and just add in our Lit Protocol SDK.
29
30
To follow this tutorial you will want to make sure you have the following:
31
32
- Node.js 18 or higher
33
- [A Free Pinata Account](https://app.pinata.cloud/register)
34
- A text editor like VSCode
35
36
Before going any further make sure you get a [free Pinata account](https://app.pinata.cloud/register) so you can make an [API key](https://docs.pinata.cloud/docs/api-keys) and get a [free Dedicated Gateway](https://docs.pinata.cloud/docs/dedicated-ipfs-gateways) for this project! Once you make an API key, save the `JWT` that we’ll use in a little bit, as well as the gateway domain for your Dedicated Gateways.
37
38
Thats it! To kick it off, simply run the command to use the Pinata Next.js Template
39
40
```bash
41
npx create-pinata-app
42
```
43
44
You will be prompted to choose your flavors of the app, such as Typescript vs Javascript, or Tailwindcss vs Vanilla CSS. For this template, I chose Typescript and Tailwindcss but feel free to choose your own. After giving it a name and making your selections go ahead and open the project in VSCode.
45
46
Back in the terminal the next thing we’re going to do is install the [Lit Protocol SDK](https://developer.litprotocol.com/v3/sdk/installation). We’ll be using the V3 of the SDK which is in beta, and you can install based on their docs [here](https://developer.litprotocol.com/v3/sdk/installation) or use this command:
47
48
```bash
49
npm install @lit-protocol/lit-node-client@cayenne
50
```
51
52
The last thing you need to do to set up the project is open the `.env.sample` file which should look like this:
53
54
```
55
PINATA_JWT=
56
NEXT_PUBLIC_GATEWAY_URL=
57
NEXT_PUBLIC_GATEWAY_TOKEN=
58
```
59
60
Paste in the `PINATA_JWT` that you made earlier when you set up your Pinata account and also paste in the `NEXT_PUBLIC_GATEWAY_URL` with the format `[https://mygateway.mypinata.cloud](https://mygateway.mypinata.cloud)` with of course your own domain URL. Then change the name of the file from `.env.sample` to `.env.local`, a very important step for our app to work!
61
62
Now lets go ahead and spin up the dev server with `npm run dev` and start building. All of our work will be done in just one file, `pages/index.tsx`; easy! We’ll start by importing the Lit Protocol SDK at the top of the file.
63
64
```jsx
65
import { useState, useRef } from "react";
66
import Head from "next/head";
67
import Image from "next/image";
68
import Files from "@/components/Files";
69
// import lit protocol sdk
70
import * as LitJsSdk from "@lit-protocol/lit-node-client";
71
```
72
73
Inside the `Home` component, we’ll add another state variable that we’ll come back to later.
74
75
```jsx
76
const [file, setFile] = useState("");
77
const [cid, setCid] = useState("");
78
const [uploading, setUploading] = useState(false);
79
// add a new state for the cid to decrypt
80
const [decryptionCid, setDecryptionCid] = useState("");
81
```
82
83
The great thing about this template is that it's already got uploads to IPFS with Pinata baked in with an `/api/files` route on the backend, so all we have to do is encrypt the file before we upload it. We’ll do this in the `uploadFile` function inside of `Home`, and it should look like this to start.
84
85
```jsx
86
const uploadFile = async (fileToUpload) => {
87
    try {
88
      setUploading(true);
89
      const formData = new FormData();
90
      formData.append("file", fileToUpload, fileToUpload.name)
91
      const res = await fetch("/api/files", {
92
        method: "POST",
93
        body: formData,
94
      });
95
      const ipfsHash = await res.text();
96
      setCid(ipfsHash);
97
      setUploading(false);
98
    } catch (e) {
99
      console.log(e);
100
      setUploading(false);
101
      alert("Trouble uploading file");
102
    }
103
  };
104
```
105
106
At the top of our `try` statement but underneath our `setUploading` state, we’ll initialize the `LitNodeClient` using the `ceyenne` network, connect our app to that network, then get the `authSig`. 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.
107
108
```jsx
109
const uploadFile = async (fileToUpload) => {
110
    try {
111
      setUploading(true);
112
      const litNodeClient = new LitJsSdk.LitNodeClient({
113
        litNetwork: 'cayenne',
114
      });
115
      // then get the authSig
116
      await litNodeClient.connect();
117
      const authSig = await LitJsSdk.checkAndSignAuthMessage({
118
        chain: 'ethereum'
119
      });
120
// rest of the code
121
```
122
123
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).
124
125
```jsx
126
const uploadFile = async (fileToUpload) => {
127
    try {
128
      setUploading(true);
129
			// Create our litNodeClient
130
      const litNodeClient = new LitJsSdk.LitNodeClient({
131
        litNetwork: 'cayenne',
132
      });
133
      // Then get the authSig
134
      await litNodeClient.connect();
135
      const authSig = await LitJsSdk.checkAndSignAuthMessage({
136
        chain: 'ethereum'
137
      });
138
			// Define our access controls, this is set to be anyone
139
			const accs = [
140
        {
141
          contractAddress: '',
142
          standardContractType: '',
143
          chain: 'ethereum',
144
          method: 'eth_getBalance',
145
          parameters: [':userAddress', 'latest'],
146
          returnValueTest: {
147
            comparator: '>=',
148
            value: '0',
149
          },
150
        },
151
      ];
152
// rest of the code
153
```
154
155
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 `encryptFileAndZipWithMetadata` method. This is handy because in order to decrypt a file, our recipient will need the `accs` 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 `authSig`, the chain, our `fileToUpload` that we passed into the function argument, the `litNodeClient`, and finally a simple `readme` that will explain to whoever happens to download it from IPFS what they need to do with it.
156
157
```jsx
158
const uploadFile = async (fileToUpload) => {
159
    try {
160
      setUploading(true);
161
      // Create our litNodeClient
162
      const litNodeClient = new LitJsSdk.LitNodeClient({
163
        litNetwork: 'cayenne',
164
      });
165
      // Then get the authSig
166
      await litNodeClient.connect();
167
      const authSig = await LitJsSdk.checkAndSignAuthMessage({
168
        chain: 'ethereum'
169
      });
170
      // Define our access controls, this is set to be anyone
171
      const accs = [
172
        {
173
          contractAddress: '',
174
          standardContractType: '',
175
          chain: 'ethereum',
176
          method: 'eth_getBalance',
177
          parameters: [':userAddress', 'latest'],
178
          returnValueTest: {
179
            comparator: '>=',
180
            value: '0',
181
          },
182
        },
183
      ];
184
      // Then we use our access controls and authSig to encrypt the file and zip it up with the metadata
185
      const encryptedZip = await LitJsSdk.encryptFileAndZipWithMetadata({
186
        accessControlConditions: accs,
187
        authSig,
188
        chain: 'ethereum',
189
        file: fileToUpload,
190
        litNodeClient: litNodeClient,
191
        readme: "Use IPFS CID of this file to decrypt it"
192
      });
193
// rest of the code
194
```
195
196
One last little touch we need to do is adapt this encrypted zip file so it will be accepted by our `/api/files` endpoint, and we’ll do so with just two lines of code.
197
198
```jsx
199
// Then we turn it into a file that will be accepted by the API endpoint
200
const encryptedBlob = new Blob([encryptedZip], { type: 'text/plain' })
201
const encryptedFile = new File([encryptedBlob], fileToUpload.name)
202
```
203
204
All together we should have an upload function that looks like this:
205
206
```jsx
207
const uploadFile = async (fileToUpload) => {
208
    try {
209
      setUploading(true);
210
      // Create our litNodeClient
211
      const litNodeClient = new LitJsSdk.LitNodeClient({
212
        litNetwork: 'cayenne',
213
      });
214
      // Then get the authSig
215
      await litNodeClient.connect();
216
      const authSig = await LitJsSdk.checkAndSignAuthMessage({
217
        chain: 'ethereum'
218
      });
219
      // Define our access controls, this is set to be anyone
220
      const accs = [
221
        {
222
          contractAddress: '',
223
          standardContractType: '',
224
          chain: 'ethereum',
225
          method: 'eth_getBalance',
226
          parameters: [':userAddress', 'latest'],
227
          returnValueTest: {
228
            comparator: '>=',
229
            value: '0',
230
          },
231
        },
232
      ];
233
      // Then we use our access controls and authSig to encrypt the file and zip it up with the metadata
234
      const encryptedZip = await LitJsSdk.encryptFileAndZipWithMetadata({
235
        accessControlConditions: accs,
236
        authSig,
237
        chain: 'ethereum',
238
        file: fileToUpload,
239
        litNodeClient: litNodeClient,
240
        readme: "Use IPFS CID of this file to decrypt it"
241
      });
242
243
      // Then we turn it into a file that will be accepted by the Pinata API
244
      const encryptedBlob = new Blob([encryptedZip], { type: 'text/plain' })
245
			const encryptedFile = new File([encryptedBlob], fileToUpload.name)
246
247
      // Finally we upload the file by passing it to our /api/files endpoint
248
      // Keep in mind this works for smaller files and you may need to do a presigned JWT and upload from the client if you're dealing with larger files
249
      // Read more about that here: https://www.pinata.cloud/blog/how-to-upload-to-ipfs-from-the-frontend-with-signed-jwts
250
      const formData = new FormData();
251
      formData.append("file", encryptedFile, encryptedFile.name)
252
      const res = await fetch("/api/files", {
253
        method: "POST",
254
        body: formData,
255
      });
256
      const ipfsHash = await res.text();
257
      setCid(ipfsHash);
258
      setUploading(false);
259
    } catch (e) {
260
      console.log(e);
261
      setUploading(false);
262
      alert("Trouble uploading file");
263
    }
264
  };
265
```
266
267
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.](https://www.pinata.cloud/blog/how-to-upload-to-ipfs-from-the-frontend-with-signed-jwts)
268
269
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 `decryptFile()`, which will take a `fileToDecrypt` CID. First thing we’ll do is fetch that file using our Dedicated Gateway and turn it into a blob.
270
271
```jsx
272
const decryptFile = async (fileToDecrypt) => {
273
    try {
274
      // First we fetch the file from IPFS using the CID and our Gateway URL, then turn it into a blob
275
      const fileRes = await fetch(`${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/${fileToDecrypt}?filename=encrypted.zip`)
276
      const file = await fileRes.blob()
277
    } catch (error) {
278
      alert("Trouble decrypting file")
279
      console.log(error)
280
    }
281
```
282
283
Now we can re-create the `litNodeClient` 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.
284
285
```jsx
286
const decryptFile = async (fileToDecrypt) => {
287
    try {
288
      // First we fetch the file from IPFS using the CID and our Gateway URL, then turn it into a blob
289
      const fileRes = await fetch(`${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/${fileToDecrypt}?filename=encrypted.zip`)
290
      const file = await fileRes.blob()
291
      // We recreated the litNodeClient and the authSig
292
      const litNodeClient = new LitJsSdk.LitNodeClient({
293
        litNetwork: 'cayenne',
294
      });
295
      await litNodeClient.connect();
296
      const authSig = await LitJsSdk.checkAndSignAuthMessage({
297
        chain: 'ethereum'
298
      });
299
    } catch (error) {
300
      alert("Trouble decrypting file")
301
      console.log(error)
302
    }
303
```
304
305
Just like we used `encryptFileAndZipWithMetadata` method, we have a matching method for decryption called `decryptZipFileWithMetadata` 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 `file` blob, our `litNodeClient`, and the recipient `authSig`. Piece of cake! From this, we’ll extract the `decryptedFile` and `metadata` from our request.
306
307
```jsx
308
const decryptFile = async (fileToDecrypt) => {
309
    try {
310
      // First we fetch the file from IPFS using the CID and our Gateway URL, then turn it into a blob
311
      const fileRes = await fetch(`${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/${fileToDecrypt}?filename=encrypted.zip`)
312
      const file = await fileRes.blob()
313
      // We recreated the litNodeClient and the authSig
314
      const litNodeClient = new LitJsSdk.LitNodeClient({
315
        litNetwork: 'cayenne',
316
      });
317
      await litNodeClient.connect();
318
      const authSig = await LitJsSdk.checkAndSignAuthMessage({
319
        chain: 'ethereum'
320
      });
321
      // Then we simpyl extract the file and metadata from the zip
322
      // We could do more with this, like try to display it in the app UI if we wanted to
323
      const { decryptedFile, metadata } = await LitJsSdk.decryptZipFileWithMetadata({
324
        file: file,
325
        litNodeClient: litNodeClient,
326
        authSig: authSig,
327
      })
328
    } catch (error) {
329
      alert("Trouble decrypting file")
330
      console.log(error)
331
    }
332
```
333
334
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.
335
336
```jsx
337
const decryptFile = async (fileToDecrypt) => {
338
    try {
339
      // First we fetch the file from IPFS using the CID and our Gateway URL, then turn it into a blob
340
      const fileRes = await fetch(`${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/${fileToDecrypt}?filename=encrypted.zip`)
341
      const file = await fileRes.blob()
342
      // We recreated the litNodeClient and the authSig
343
      const litNodeClient = new LitJsSdk.LitNodeClient({
344
        litNetwork: 'cayenne',
345
      });
346
      await litNodeClient.connect();
347
      const authSig = await LitJsSdk.checkAndSignAuthMessage({
348
        chain: 'ethereum'
349
      });
350
      // Then we simpyl extract the file and metadata from the zip
351
      // We could do more with this, like try to display it in the app UI if we wanted to
352
      const { decryptedFile, metadata } = await LitJsSdk.decryptZipFileWithMetadata({
353
        file: file,
354
        litNodeClient: litNodeClient,
355
        authSig: authSig,
356
      })
357
      // After we have our dcypted file we can download it
358
      const blob = new Blob([decryptedFile], { type: 'application/octet-stream' });
359
      const downloadLink = document.createElement('a');
360
      downloadLink.href = URL.createObjectURL(blob);
361
      downloadLink.download = metadata.name;  // Use the metadata to get the file name and type
362
363
    } catch (error) {
364
      alert("Trouble decrypting file")
365
      console.log(error)
366
    }
367
```
368
369
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.
370
371
```jsx
372
<input
373
  type="text"
374
  onChange={(e) => setDecryptionCid(e.target.value)}
375
  className="px-4 py-2 border-2 border-secondary rounded-3xl text-lg"
376
  placeholder="Enter CID to decrypt"
377
/>
378
<button
379
  onClick={() => decryptFile(decryptionCid)}
380
  className="mr-10 w-[150px] bg-light text-secondary border-2 border-secondary rounded-3xl py-2 px-4 hover:bg-secondary hover:text-light transition-all duration-300 ease-in-out"
381
>Decrypt</button>
382
```
383
384
With all of this together, you should have an app with the following flow!
385
386
<video muted autoplay style="width: 100%; height: auto; position: relative;">
387
    <source src="https://mktg.mypinata.cloud/ipfs/QmcoP5gj1gJyZNCDUHE2e1g1cbwvEHo6E56xMhPV8KaHaf/file-encryption.mp4" type="video/mp4">
388
</video>
389
390
You can also download and use this exact template [here](https://github.com/PinataCloud/pinata-lit-protocol-template)!
391
392
## Going Further
393
394
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](https://developer.litprotocol.com/v3/sdk/access-control/evm/basic-examples) you can do. For example, if you only wanted holders of a particular ERC721 NFT you could use the following.
395
396
```jsx
397
const accessControlConditions = [
398
  {
399
    contractAddress: '0xA80617371A5f511Bf4c1dDf822E6040acaa63e71',
400
    standardContractType: 'ERC721',
401
    chain,
402
    method: 'balanceOf',
403
    parameters: [
404
      ':userAddress'
405
    ],
406
    returnValueTest: {
407
      comparator: '>',
408
      value: '0'
409
    }
410
  }
411
]
412
```
413
414
Or you could do DAO membership (MolochDAOv2.1, also supports DAOHaus)****[](https://developer.litprotocol.com/v3/sdk/access-control/evm/basic-examples#must-be-a-member-of-a-dao-molochdaov21-also-supports-daohaus)****
415
416
```jsx
417
const accessControlConditions = [
418
  {
419
    contractAddress: '0x50D8EB685a9F262B13F28958aBc9670F06F819d9',
420
    standardContractType: 'MolochDAOv2.1',
421
    chain,
422
    method: 'members',
423
    parameters: [
424
      ':userAddress',
425
    ],
426
    returnValueTest: {
427
      comparator: '=',
428
      value: 'true'
429
    }
430
  }
431
]
432
```
433
434
You can even do a simple check if the recipient is a particular wallet address.
435
436
```jsx
437
const accessControlConditions = [
438
  {
439
    contractAddress: '',
440
    standardContractType: '',
441
    chain,
442
    method: '',
443
    parameters: [
444
      ':userAddress',
445
    ],
446
    returnValueTest: {
447
      comparator: '=',
448
      value: '0x50e2dac5e78B5905CB09495547452cEE64426db2'
449
    }
450
  }
451
]
452
```
453
454
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!
455
456
Happy Pinning!