HANDS ON
Encrypting data in the browser

Encrypting onchain data (browser-based)

This guide teaches you how to:

  • Encrypt a File (opens in a new tab) uploaded from the browser using Lit Protocol
  • Establish a set of rules determining who can decrypt the File
  • Store the encrypted File on Arweave using Irys
  • Decrypt the File using Lit Protocol
  • Display the decrypted File in the browser

Before diving into this guide, begin with "Encrypting onchain data".

ℹ️

The Irys Provenance Toolkit features an encrypted uploader component (opens in a new tab) that allows for file uploads directly from the browser, handles its encryption and decryption, and displays the decrypted file. When building your project, consider cloning the Toolkit repository (opens in a new tab) to use as a foundation.

Dependencies

Install using npm:

npm install @irys/sdk @lit-protocol/lit-node-client@^3 ethers@^5

or yarn:

yarn add @irys/sdk @lit-protocol/lit-node-client@^3 ethers@^5

Imports

import * as LitJsSdk from "@lit-protocol/lit-node-client";
import { AccessControlConditions, ILitNodeClient } from "@lit-protocol/types";
import { checkAndSignAuthMessage } from "@lit-protocol/lit-node-client";
import { WebIrys } from "@irys/sdk";

Encrypting a File

File uploader

Add a form to an HTML page that accepts a file as input:

<form id="uploadForm">
	<input type="file" name="fileToUpload" id="fileToUpload" accept="*/*" required />
	<input type="button" value="Upload File" onclick="handleUpload()" />
</form>

Wallet signature

Use the Lit SDK function checkAndSignAuthMessage() (opens in a new tab) to prompt the user to sign a basic transaction, confirming wallet ownership. Authentication details are then saved in the browser's local storage, future calls to checkAndSignAuthMessage() will use the stored version if present.

const authSig = await checkAndSignAuthMessage({
	chain: process.env.NEXT_PUBLIC_LIT_CHAIN || "polygon",
});

Lit node

Connect to a Lit node:

const litNodeClient = new LitJsSdk.LitNodeClient({
	litNetwork: "cayenne",
});
await litNodeClient.connect();

Access control conditions

Define rules for who to decrypt your data (opens in a new tab), limiting it to anyone with >= 0 ETH:

// This defines who can decrypt the data
function getAccessControlConditions() {
	const accessControlConditions = [
		{
			contractAddress: "",
			standardContractType: "",
			chain: "ethereum",
			method: "eth_getBalance",
			parameters: [":userAddress", "latest"],
			returnValueTest: {
				comparator: ">=",
				value: "0", // 0 ETH, so anyone can open
			},
		},
	];
 
	return accessControlConditions;
}

To the wallet 0x50e2dac5e78B5905CB09495547452cEE64426db2

const accessControlConditions = [
	{
		contractAddress: "",
		standardContractType: "",
		chain,
		method: "",
		parameters: [":userAddress"],
		returnValueTest: {
			comparator: "=",
			value: "0x50e2dac5e78B5905CB09495547452cEE64426db2",
		},
	},
];

Or by people who hold a given ERC721:

const accessControlConditions = [
	{
		contractAddress: "0xA80617371A5f511Bf4c1dDf822E6040acaa63e71",
		standardContractType: "ERC721",
		chain,
		method: "balanceOf",
		parameters: [":userAddress"],
		returnValueTest: {
			comparator: ">",
			value: "0",
		},
	},
];

Encrypting

Finally, encrypt the File using Lit's encryptFileAndZipWithMetadata() (opens in a new tab) function.

This function takes the File object, encrypts it and stores it in a single zip file with all metadata.

// Create a zip blob containing the encrypted file and associated metadata
const zipBlob = await LitJsSdk.encryptFileAndZipWithMetadata({
	chain: process.env.NEXT_PUBLIC_LIT_CHAIN || "polygon",
	authSig,
	accessControlConditions,
	file,
	litNodeClient,
	readme: "This file was encrypted using LitProtocol and the Irys Provenance Toolkit.",
});

Storing on Arweave via Irys

Once encrypted, use webIrys.uploadFile() to upload the zip blob. In this case, we tag the upload with a tag indicating the content type of the underlying file, and another tag letting us know it's encrypted.

// Tag the upload marking it as
// - Binary file
// - Containing a file of type file.type (used when displaying)
// - Encrypted (used by our display code)
const tags: Tag[] = [
	{
		name: "Content-Type",
		value: "application/octet-stream",
	},
	{
		name: "Encrypted-File-Content-Type",
		value: file.type,
	},
	{
		name: "Irys-Encrypted",
		value: "true",
	},
];
 
const receipt = await irys.uploadFile(file, {
	tags,
});

Combined file

Then, combine everything in a single file:

async function encryptFile(file: File) {
	// 1. Connect to a Lit node
	const litNodeClient = new LitJsSdk.LitNodeClient({
		litNetwork: "cayenne",
	});
	await litNodeClient.connect();
 
	// 2. Ensure we have a wallet signature
	const authSig = await checkAndSignAuthMessage({
		chain: "ethereum",
		nonce: await litNodeClient.getLatestBlockhash(),
	});
 
	// 3. Define access control conditions.
	// This defines who can decrypt, current settings allow for
	// anyone with a ETH balance >= 0 to decrypt, which
	// means that anyone can. This is for demo purposes.
	const accessControlConditions = [
		{
			contractAddress: "",
			standardContractType: "",
			chain: "ethereum",
			method: "eth_getBalance",
			parameters: [":userAddress", "latest"],
			returnValueTest: {
				comparator: ">=",
				value: "0",
			},
		},
	];
 
	// 4. Create a zip blob containing the encrypted file and associated metadata
	const zipBlob = await LitJsSdk.encryptFileAndZipWithMetadata({
		chain: process.env.NEXT_PUBLIC_LIT_CHAIN || "ethereum",
		authSig,
		accessControlConditions,
		file,
		litNodeClient,
		readme: "This file was encrypted using LitProtocol and the Irys Provenance Toolkit.",
	});
 
	return zipBlob;
}
 
// Uploads the encrypted File (with metadata) to Irys
async function uploadFile(file: File): Promise<string> {
	const irys = await getIrys();
 
	try {
		const price = await irys.getPrice(file?.size);
		const balance = await irys.getLoadedBalance();
 
		if (price.isGreaterThanOrEqualTo(balance)) {
			console.log("Funding node.");
			await irys.fund(price);
		} else {
			console.log("Funding not needed, balance sufficient.");
		}
 
		// Tag the upload marking it as
		// - Binary file
		// - Containing a file of type file.type (used when displaying)
		// - Encrypted (used by our display code)
		const tags: Tag[] = [
			{
				name: "Content-Type",
				value: "application/octet-stream",
			},
			{
				name: "Encrypted-File-Content-Type",
				value: file.type,
			},
			{
				name: "Irys-Encrypted",
				value: "true",
			},
		];
 
		const receipt = await irys.uploadFile(file, {
			tags,
		});
		console.log(`Uploaded successfully. ${GATEWAY_BASE}${receipt.id}`);
 
		return receipt.id;
	} catch (e) {
		console.log("Error uploading single file ", e);
	}
	return "";
}
 
// Encrypts and then uploads a File
async function encryptAndUploadFile(file: File): Promise<string> {
	const encryptedData = await encryptFile(file);
	return await uploadFile(encryptedData);
}

And call it from your HTML form:

<form id="uploadForm">
	<input type="file" name="fileToUpload" id="fileToUpload" accept="*/*" required />
	<input type="button" value="Upload File" onclick="handleUpload()" />
</form>
 
<script>
	document.getElementById("uploadForm").addEventListener("submit", function (event) {
		event.preventDefault();
		handleUpload();
	});
 
	async function handleUpload() {
		const fileInput = document.getElementById("fileToUpload");
		const file = fileInput.files[0];
		if (file) {
			const result = await encryptAndUploadFile(file);
		} else {
			alert("Please select a file to upload.");
		}
	}
</script>

Decrypting a File

To decrypt and display the image:

  1. Load the data from the Irys gateway
  2. Extract the zip blob
  3. Decrypt it
async function decryptFile(id: string, encryptedFileType: string): Promise<string> {
	try {
		// 1. Retrieve the file from https://gateway.irys.xyz/${id}
		const response = await fetch(`${GATEWAY_BASE}${id}`);
		if (!response.ok) {
			throw new Error(`Failed to fetch encrypted file from gateway with ID: ${id}`);
		}
 
		// 2. Extract the zipBlob
		const zipBlob = await response.blob();
 
		// 3. Connect to a Lit node
		const litNodeClient = new LitJsSdk.LitNodeClient({
			litNetwork: "cayenne",
		});
		await litNodeClient.connect();
 
		// 3.5 Get a reference to an AuthSig (if in local storage that will be used instead of prompting the user to sign)
		const authSig = await checkAndSignAuthMessage({
			chain: "ethereum",
			nonce: await litNodeClient.getLatestBlockhash(),
		});
 
		// 4. Decrypt the zipBlob
		const result = await LitJsSdk.decryptZipFileWithMetadata({
			file: zipBlob,
			litNodeClient: litNodeClient,
			authSig: authSig, // Include this only if necessary
		});
		const decryptedFile = result.decryptedFile;
		// 5. Convert to a blob
		const blob = arrayBufferToBlob(decryptedFile, encryptedFileType);
		// 6. Build a dynamic URL
		const dataUrl = await blobToDataURL(blob);
 
		return dataUrl;
	} catch (e) {
		console.error("Error decrypting file:", e);
	}
	return "";
}

Displaying encrypted File in the browser

After decrypting the image file, you need to convert the data blob to a URL with the format data:image/png;base64,[base64-encoded-data] before setting it as the src attribute of an <img> element.

These functions assist in converting the data blob to a URL.

// Helper functions for use in showing decrypted images
function arrayBufferToBlob(buffer: ArrayBuffer, type: string): Blob {
	return new Blob([buffer], { type: type });
}
 
function blobToDataURL(blob: Blob): Promise<string> {
	return new Promise((resolve, reject) => {
		const reader = new FileReader();
		reader.onload = (event) => {
			if (event.target?.result) {
				resolve(event.target.result as string);
			} else {
				reject(new Error("Failed to read blob as Data URL"));
			}
		};
		reader.readAsDataURL(blob);
	});
}

Full code

All of this code is contained in a single file as part of the Provenance Toolkit (opens in a new tab).

Server-side example

Demo