This document explains how to perform re-encryption. Re-encryption is required when you want a user to access their private data without it being exposed to the blockchain.
Re-encryption in fhEVM enables the secure sharing or reuse of encrypted data under a new public key without exposing the plaintext. This feature is essential for scenarios where encrypted data must be transferred between contracts, dApps, or users while maintaining its confidentiality.
Re-encryption is particularly useful for allowing individual users to securely access and decrypt their private data, such as balances or counters, while maintaining data confidentiality.
Overview
The re-encryption process involves retrieving ciphertext from the blockchain and performing re-encryption on the client-side. In other words we take the data that has been encrypted by the KMS, decrypt it and encrypt it with the users private key, so only he can access the information.
This ensures that the data remains encrypted under the blockchain’s FHE key but can be securely shared with a user by re-encrypting it under the user’s NaCl public key.
Re-encryption is facilitated by the Gateway and the Key Management System (KMS). The workflow consists of the following:
Retrieving the ciphertext from the blockchain using a contract’s view function.
Re-encrypting the ciphertext client-side with the user’s public key, ensuring only the user can decrypt it.
Step 1: retrieve the ciphertext
To retrieve the ciphertext that needs to be re-encrypted, you can implement a view function in your smart contract. Below is an example implementation:
Here, balanceOf allows retrieval of the user’s encrypted balance stored on the blockchain.
Step 2: re-encrypt the ciphertext
Re-encryption is performed client-side using the fhevmjs library. Refer to the guide to learn how to include fhevmjs in your project. Below is an example of how to implement reencryption in a dApp:
import { createInstances } from"../instance";import { getSigners, initSigners } from"../signers";import abi from"./abi.json";import { Contract, BrowserProvider } from"ethers";import { createInstance } from"fhevmjs/bundle";constCONTRACT_ADDRESS="";constprovider=newBrowserProvider(window.ethereum);constaccounts=awaitprovider.send("eth_requestAccounts", []);constUSER_ADDRESS= accounts[0];awaitinitSigners(); // Initialize signersconstsigners=awaitgetSigners();constinstance=awaitcreateInstances(this.signers);// Generate the private and public key, used for the reencryptionconst { publicKey,privateKey } =instance.generateKeypair();// Create an EIP712 object for the user to sign.consteip712=instance.createEIP712(publicKey,CONTRACT_ADDRESS);// Request the user's signature on the public keyconstparams= [USER_ADDRESS,JSON.stringify(eip712)];constsignature=awaitwindow.ethereum.request({ method:"eth_signTypedData_v4", params });// Get the ciphertext to reencryptconstConfidentialERC20=newContract(CONTRACT_ADDRESS, abi, signer).connect(provider);constencryptedBalance=ConfidentialERC20.balanceOf(userAddress);// This function will call the gateway and decrypt the received value with the provided private keyconstuserBalance=instance.reencrypt( encryptedBalance,// the encrypted balance privateKey,// the private key generated by the dApp publicKey,// the public key generated by the dApp signature,// the user's signature of the public keyCONTRACT_ADDRESS,// The contract address where the ciphertext isUSER_ADDRESS,// The user address where the ciphertext is);console.log(userBalance);
This code retrieves the user’s encrypted balance, re-encrypts it with their public key, and decrypts it on the client-side using their private key.
Key additions to the code
instance.generateKeypair(): Generates a public-private keypair for the user.
instance.createEIP712(publicKey, CONTRACT_ADDRESS): Creates an EIP712 object for signing the user’s public key.
instance.reencrypt(): Facilitates the re-encryption process by contacting the Gateway and decrypting the data locally with the private key.
Applying re-encryption to the counter example
Here’s an enhanced Encrypted Counter example where each user maintains their own encrypted counter. Re-encryption is used to securely share counter values with individual users.
Encrypted counter with re-encryption
// SPDX-License-Identifier: MITpragmasolidity ^0.8.24;import"fhevm/lib/TFHE.sol";import { SepoliaZamaFHEVMConfig } from"fhevm/config/ZamaFHEVMConfig.sol";/// @title EncryptedCounter4/// @notice A contract that maintains encrypted counters for each user and is meant for demonstrating how re-encryption works/// @dev Uses TFHE library for fully homomorphic encryption operations/// @custom:security Each user can only access and modify their own counter/// @custom:experimental This contract is experimental and uses FHE technologycontractEncryptedCounter4isSepoliaZamaFHEVMConfig {// Mapping from user address to their encrypted counter valuemapping(address=> euint8) private counters;functionincrementBy(einput amount,bytescalldata inputProof) public {// Initialize counter if it doesn't existif (!TFHE.isInitialized(counters[msg.sender])) { counters[msg.sender] = TFHE.asEuint8(0); }// Convert input to euint8 and add to sender's counter euint8 incrementAmount = TFHE.asEuint8(amount, inputProof); counters[msg.sender] = TFHE.add(counters[msg.sender], incrementAmount); TFHE.allowThis(counters[msg.sender]); TFHE.allow(counters[msg.sender], msg.sender); }functiongetCounter() publicviewreturns (euint8) {// Return the encrypted counter value for the senderreturn counters[msg.sender]; }}
Frontend code of re-encryption / tests for EncryptedCounter4
Here’s a sample test to verify re-encryption functionality:
import { createInstance } from"../instance";import { reencryptEuint8 } from"../reencrypt";import { getSigners, initSigners } from"../signers";import { expect } from"chai";import { ethers } from"hardhat";describe("EncryptedCounter4",function () {before(asyncfunction () {awaitinitSigners(); // Initialize signersthis.signers =awaitgetSigners(); });beforeEach(asyncfunction () {constCounterFactory=awaitethers.getContractFactory("EncryptedCounter4");this.counterContract =awaitCounterFactory.connect(this.signers.alice).deploy();awaitthis.counterContract.waitForDeployment();this.contractAddress =awaitthis.counterContract.getAddress();this.instances =awaitcreateInstance(); });it("should allow reencryption and decryption of counter value",asyncfunction () {constinput=this.instances.createEncryptedInput(this.contractAddress,this.signers.alice.address);input.add8(1); // Increment by 1 as an exampleconstencryptedAmount=awaitinput.encrypt();// Call incrementBy with encrypted amountconsttx=awaitthis.counterContract.incrementBy(encryptedAmount.handles[0],encryptedAmount.inputProof);awaittx.wait();// Get the encrypted counter valueconstencryptedCounter=awaitthis.counterContract.getCounter();constdecryptedValue=awaitreencryptEuint8(this.signers,this.instances,"alice", encryptedCounter,this.contractAddress, );// Verify the decrypted value is 1 (since we incremented once)expect(decryptedValue).to.equal(1); });it("should allow reencryption of counter value",asyncfunction () {constinput=this.instances.createEncryptedInput(this.contractAddress,this.signers.bob.address);input.add8(1); // Increment by 1 as an exampleconstencryptedAmount=awaitinput.encrypt();// Call incrementBy with encrypted amountconsttx=awaitthis.counterContract.connect(this.signers.bob).incrementBy(encryptedAmount.handles[0],encryptedAmount.inputProof);awaittx.wait();// Get the encrypted counter valueconstencryptedCounter=awaitthis.counterContract.connect(this.signers.bob).getCounter();constdecryptedValue=awaitreencryptEuint8(this.signers,this.instances,"bob", encryptedCounter,this.contractAddress, );// Verify the decrypted value is 1 (since we incremented once)expect(decryptedValue).to.equal(1); });});
Key additions in testing
setupReencryption(): Prepares the re-encryption process by generating keys and a signature for the user.
instance.reencrypt(): Facilitates re-encryption and local decryption of the data for testing purposes.
Validation: Confirms that the decrypted counter matches the expected value.