Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
LABS
Guides

Setting Up Encrypted User Profiles on a Public Blockchain

A technical tutorial for developers to implement verifiable yet private user profiles using decentralized storage, on-chain pointers, and asymmetric encryption for selective disclosure.
Chainscore © 2026
introduction
TUTORIAL

Introduction to Encrypted On-Chain Profiles

A guide to building private, user-controlled identity layers on public blockchains using zero-knowledge proofs and encryption.

An encrypted on-chain profile is a data structure stored on a public blockchain where the core user data is encrypted, not visible in plaintext. Unlike traditional profiles where information like a username or bio is public, these systems use cryptographic techniques to ensure privacy. The public blockchain provides decentralized storage and verification, while the encryption ensures only the user or authorized parties can access the underlying data. This model is foundational for applications requiring user identity, reputation, or credentials without sacrificing the transparency and security guarantees of a public ledger.

The core technical components enabling this are asymmetric encryption and zero-knowledge proofs (ZKPs). A user generates a public/private key pair, with the public key serving as their persistent on-chain identifier. Profile data is encrypted with this public key, meaning only the holder of the corresponding private key can decrypt it. To prove attributes about the encrypted data—such as being over 18 or holding a specific credential—without revealing the data itself, the user generates a ZKP. Protocols like Semaphore or zkSNARKs circuits allow a user to prove membership in a group or validity of a claim with a single, verifiable proof.

Setting up a basic profile involves a few key steps. First, a user generates an Ed25519 or secp256k1 key pair client-side; the private key never leaves their device. The public key is committed to the blockchain, often within a smart contract registry. To store data, the user encrypts a JSON profile object using their own public key (or a shared key for specific recipients) with a library like libsodium.js. The resulting ciphertext is stored on-chain, typically via a transaction to a profile manager contract, or off-chain on a decentralized storage network like IPFS or Arweave, with only the content hash stored on-chain.

Here is a simplified conceptual example of creating and storing an encrypted profile using Ethereum and IPFS:

javascript
// 1. Generate key pair (client-side)
const keyPair = generateKeyPair(); // e.g., using libsodium
const publicKey = keyPair.publicKey;

// 2. Create profile data object
const profileData = {
  name: "Alice",
  email: "alice@example.com",
  age: 30
};

// 3. Encrypt data with own public key
const encryptedData = seal(JSON.stringify(profileData), publicKey);

// 4. Store ciphertext on IPFS, get CID
const ipfsCid = await ipfsClient.add(encryptedData);

// 5. Store public key & IPFS CID on-chain
await profileContract.createProfile(publicKey, ipfsCid);

The smart contract now maps the user's wallet address to their public key and the reference to the encrypted data. To read the profile, the user would fetch the CID from the contract, retrieve the ciphertext from IPFS, and decrypt it locally with their private key.

Major use cases for encrypted profiles include private decentralized identity (DID), on-chain reputation systems, and gated access controls. For instance, a DeFi protocol could grant loan terms based on a private, verified credit score stored in an encrypted profile. A DAO could use them for anonymous yet sybil-resistant voting. The Ethereum Attestation Service (EAS) schema registry can be used alongside encryption to create verifiable, private attestations about a profile. The key advantage is decoupling: the blockchain provides a global, immutable registry and verification layer, while the encryption layer ensures user sovereignty over personal data.

When implementing this pattern, developers must consider key management, gas costs for on-chain storage, and data availability. Storing large ciphertexts directly on-chain is prohibitively expensive, hence the common hybrid approach with IPFS. Users must securely back up their private keys, as loss means permanent loss of access. Furthermore, the security of the system depends entirely on the underlying encryption (like X25519 for key exchange and XSalsa20-Poly1305 for encryption) and the correct implementation of ZKP circuits. Audited libraries and frameworks, such as those from the Privacy & Scaling Explorations (PSE) team, are recommended for production use.

prerequisites
PREREQUISITES AND SETUP

Setting Up Encrypted User Profiles on a Public Blockchain

This guide covers the foundational steps for building private user profiles on transparent ledgers using zero-knowledge cryptography and decentralized storage.

Building encrypted user profiles on a public blockchain like Ethereum or Solana requires a multi-layered architecture. The core challenge is storing private data off-chain while anchoring cryptographic proofs on-chain. You will need a decentralized storage layer such as IPFS, Arweave, or Ceramic for the encrypted data, and a zero-knowledge proof system like Semaphore, zk-SNARKs, or Noir to manage identity and permissions. The on-chain component is typically a lightweight registry smart contract that stores only public keys, commitment hashes, and proof verifiers.

Before writing any code, set up your development environment. You will need Node.js (v18+), a package manager like npm or yarn, and a code editor. For blockchain interaction, install a wallet library such as ethers.js or wagmi, and a development framework like Hardhat or Foundry for Ethereum, or Anchor for Solana. For zero-knowledge development, tools like circom and snarkjs are essential for circuit compilation and proof generation. Start by initializing a new project and installing these core dependencies.

The first technical step is to define your data schema and encryption strategy. User profile data—like a username, bio, or avatar link—should be serialized into a structured format (e.g., JSON). This data is then encrypted using a symmetric key, which is itself encrypted to the user's public key for secure storage. A common pattern is to use the eth-encrypt library or the nacl box for encryption. The resulting ciphertext is uploaded to your chosen decentralized storage network, which returns a Content Identifier (CID).

Next, you must implement the zero-knowledge component. This involves creating a circuit that proves a user knows a secret (their private key) corresponding to a public commitment on-chain, without revealing it. For a group membership use case, you might use Semaphore. You'll write a circuit in a domain-specific language like Circom, compile it to generate proving and verification keys, and deploy a verifier contract. The user's client-side application will generate proofs locally using these keys to authorize profile updates.

Finally, integrate the components in a front-end application. Use a framework like Next.js or Vite. The user flow involves: connecting a wallet, generating encryption keys, uploading encrypted data to IPFS, creating a zero-knowledge proof of profile ownership, and submitting a transaction to your registry contract. The contract call should include the storage CID and the ZK proof. Always test thoroughly on a testnet like Sepolia or Solana Devnet before considering mainnet deployment to ensure privacy guarantees hold.

key-concepts
ENCRYPTED USER PROFILES

Core Architectural Components

Building private user profiles on a public ledger requires a specific stack of cryptographic primitives and data structures. These components handle identity, encryption, and selective data disclosure.

how-it-works-text
SYSTEM ARCHITECTURE AND DATA FLOW

Setting Up Encrypted User Profiles on a Public Blockchain

This guide details the architectural patterns for building private, user-controlled profiles on transparent ledgers using zero-knowledge proofs and selective disclosure.

Public blockchains like Ethereum offer permanence and censorship resistance but expose all data. For user profiles containing personal details, this is problematic. The core architectural challenge is storing sensitive data off-chain while anchoring cryptographic commitments on-chain. A common pattern uses a decentralized storage network like IPFS or Arweave for the encrypted data blob, with only its content identifier (CID) and an encryption key commitment stored in a smart contract. The user's client—typically a wallet—holds the decryption key, ensuring data sovereignty.

Encryption alone isn't sufficient for selective sharing. To prove specific attributes (e.g., being over 18) without revealing the underlying data (your birth date), systems integrate zero-knowledge proofs (ZKPs). A user generates a ZK-SNARK or ZK-STARK proof off-chain that attests a statement about their private data is true. This proof, which is small and verifiable on-chain, is submitted alongside the relevant transaction. Libraries like circom and snarkjs are used to define these proof circuits. The on-chain verifier contract checks the proof against the public parameters stored during a trusted setup.

The data flow for updating a profile begins client-side. The user encrypts their new profile JSON with a symmetric key, uploads the ciphertext to IPFS, and receives a CID. They then create a transaction to the profile manager contract, calling updateProfile(CID, newKeyCommitment). The keyCommitment is often a hash of the encryption key, which can later be revealed selectively. To share an attribute, the flow shifts: the user's wallet generates a ZKP locally using their private data and key, then sends the proof to a verifier contract or directly to a relying party.

Key management is critical. Losing the encryption key means losing access to the profile data permanently, as the blockchain only stores commitments. Architectures often derive this key from the user's wallet signature via eth_sign, or use key derivation functions tied to the wallet's private key. For recovery, some systems implement social recovery schemes or use encrypted key shares distributed among trusted guardians, with the shares themselves stored on-chain or in a decentralized storage network.

Real-world implementations like Sismo's ZK Badges or Disco's Data Backpack demonstrate this architecture. They use Ethereum for attestation anchoring and verification, Ceramic/IPFS for mutable data streams, and ZK circuits for privacy. When designing your system, audit the trust assumptions: who runs the storage nodes, who performed the ZK trusted setup, and the security of the key derivation path. The end goal is a user-centric architecture where profiles are portable, private, and interoperable across applications without relying on a central database.

COMPARISON

Decentralized Storage: IPFS vs. Arweave

Key differences between the two primary decentralized storage protocols for building encrypted user profiles.

Feature / MetricIPFS (InterPlanetary File System)Arweave

Data Persistence Model

Peer-to-peer pinning; data can be ephemeral

Permanent, one-time payment for perpetual storage

Primary Use Case

Content-addressed data distribution and caching

Permanent data archiving and immutable storage

Cost Model

Variable (pinning services, node operation)

One-time, upfront fee (approx. $0.02 - $0.10 per MB)

Data Mutability

Immutable by CID; new data = new CID

Fully immutable; data cannot be altered or deleted

Encryption Support

Client-side encryption required before upload

Client-side encryption required before upload

Retrieval Speed

Depends on node availability and caching

Consistent, incentivized by mining rewards

Suitability for Private Profiles

High (with pre-upload encryption)

High (with pre-upload encryption)

Native Blockchain Integration

Content addressing only; no consensus

Uses its own proof-of-access blockchain

step-1-encrypt-store
BUILDING PRIVATE PROFILES

Step 1: Encrypting Data and Uploading to Decentralized Storage

Learn how to encrypt sensitive user data off-chain and store it on decentralized storage networks like IPFS or Arweave, creating a private foundation for on-chain profiles.

Storing raw personal data directly on a public blockchain like Ethereum is problematic. Every transaction is permanently visible, making sensitive information like email addresses or private messages accessible to anyone. The solution is a hybrid approach: encrypt data off-chain and store only the encrypted result and a content identifier on-chain. This preserves user privacy while leveraging blockchain for data availability and integrity verification. The encrypted data blob is hosted on a decentralized storage network, ensuring it is censorship-resistant and accessible without relying on a central server.

For encryption, we use symmetric cryptography with a key derived from the user's wallet. A common pattern is to generate an encryption key from a signed message or a decentralized identifier (DID). Libraries like ethers.js and lit-protocol facilitate this. For example, you can encrypt data using the lit-js-sdk with a condition that only the user's wallet address can decrypt it. The resulting ciphertext is then uploaded to a storage service like IPFS via Pinata or Arweave using their respective SDKs, which return a unique Content Identifier (CID) or transaction ID.

Here is a simplified code snippet using ethers and the Pinata SDK for IPFS:

javascript
import { ethers } from 'ethers';
import pinataSDK from '@pinata/sdk';
// 1. User signs a message to derive a key
const signer = new ethers.Wallet(privateKey);
const message = 'Encrypt my profile data';
const signature = await signer.signMessage(message);
// 2. Encrypt the profile JSON using the signature hash as a key (simplified)
const profileData = { email: 'user@example.com', name: 'Alice' };
const ciphertext = encryptData(JSON.stringify(profileData), signature);
// 3. Upload ciphertext to IPFS
const pinata = new pinataSDK(apiKey, apiSecret);
const ipfsResult = await pinata.pinJSONToIPFS({ data: ciphertext });
const contentIdentifier = ipfsResult.IpfsHash; // This CID goes on-chain

The returned Content Identifier (CID) is a cryptographic hash of the encrypted data. This hash is what you store in your smart contract. For instance, a profile contract might have a mapping like mapping(address => string) public userCid. Storing the CID on-chain creates an immutable, verifiable pointer to the data. Anyone can fetch the encrypted data from IPFS using the CID, but only the user who holds the private key corresponding to the encryption conditions can decrypt and read the original content. This pattern is fundamental to building self-sovereign identity systems.

Choosing a storage layer involves trade-offs. IPFS offers content-addressed storage but does not guarantee persistence unless pinned by a service. Arweave provides permanent storage for a one-time fee, making it suitable for long-term data. Ceramic Network offers a more structured, stream-based protocol for mutable data. For production applications, consider using a decentralized access control layer like Lit Protocol or threshold cryptography to manage encryption keys and enable secure sharing without exposing private keys, moving beyond the basic example shown above.

step-2-on-chain-pointer
ON-CHAIN VERIFICATION

Step 2: Anchoring the CID with a Smart Contract

This step creates a permanent, verifiable link between a user's public identifier and their encrypted profile data stored on IPFS.

After encrypting your profile data and uploading it to IPFS, you receive a Content Identifier (CID). This CID is a cryptographic hash that uniquely represents your data. However, the CID alone on IPFS is not discoverable or verifiable on-chain. To solve this, you anchor the CID to a public blockchain using a smart contract. This creates an immutable record that links your on-chain identity (like an Ethereum address) to the off-chain data. Think of it as publishing a public directory entry that points to your private, encrypted profile.

The anchoring smart contract is typically simple and gas-efficient. Its core function is to store a mapping from a user's address to their latest profile CID. A basic Solidity implementation might look like this:

solidity
contract ProfileRegistry {
    mapping(address => string) public cidRegistry;
    
    event ProfileUpdated(address indexed user, string cid);
    
    function setProfileCID(string calldata _cid) external {
        cidRegistry[msg.sender] = _cid;
        emit ProfileUpdated(msg.sender, _cid);
    }
    
    function getProfileCID(address _user) external view returns (string memory) {
        return cidRegistry[_user];
    }
}

When a user calls setProfileCID, they pay a gas fee to store their current CID. The contract emits an event, making the update easily indexable by off-chain services.

This architecture provides several key benefits. First, it establishes data sovereignty—users control their profile by controlling the private key that calls the smart contract. Second, it enables selective disclosure. Users can prove they own a profile by signing a message with their key, without revealing the private data inside. Third, it allows for versioning. Each call to setProfileCID updates the pointer, enabling users to publish new versions of their profile while maintaining a full history via the blockchain's immutable event logs. This pattern is foundational for decentralized identity systems like ERC-725 and Verifiable Credentials.

step-3-key-sharing
PRIVACY PATTERNS

Step 3: Implementing Selective Key Sharing

This step details how to grant specific users access to encrypted profile data using public-key cryptography and smart contract permissions.

Selective key sharing is the mechanism that enables privacy on a public ledger. Instead of storing sensitive data in plaintext, you encrypt it with a symmetric key (e.g., using AES-256-GCM). This data_encryption_key is then itself encrypted with the public key of each authorized user. The resulting encrypted keys, or wrapped keys, are the only user-specific data stored on-chain. This pattern ensures the encrypted profile data blob is static and can be replicated across nodes, while access is dynamically controlled by the list of wrapped keys.

Implementing this requires a smart contract to manage permissions. A common approach is a registry contract that maps a user's identifier (like their Ethereum address) to their encrypted data hash and an array of wrapped keys. The core functions are grantAccess(address viewer, bytes wrappedKey) and revokeAccess(address viewer). The wrappedKey is the data_encryption_key encrypted to the viewer's public key, typically performed off-chain using a library like eth-sig-util. The contract must also emit events for access changes to enable efficient off-chain indexing by applications.

Here is a simplified Solidity example for the access control logic:

solidity
mapping(address => bytes) public encryptedData;
mapping(address => mapping(address => bytes)) public accessKeys;

event AccessGranted(address profileOwner, address viewer);
event AccessRevoked(address profileOwner, address viewer);

function grantAccess(address viewer, bytes calldata wrappedKey) external {
    accessKeys[msg.sender][viewer] = wrappedKey;
    emit AccessGranted(msg.sender, viewer);
}

function revokeAccess(address viewer) external {
    delete accessKeys[msg.sender][viewer];
    emit AccessRevoked(msg.sender, viewer);
}

The profile owner (msg.sender) pays gas to update their permission set. A frontend dApp would fetch the wrappedKey for a user and use their private key (via a wallet) to decrypt it, obtaining the data_encryption_key to finally decrypt the profile data.

Key management is critical. The data_encryption_key should be generated client-side and never transmitted in plaintext. Libraries like libsodium.js or ethers.js provide robust encryption utilities. For production systems, consider using a key derivation function (KDF) to create the data key from a master secret, allowing for key rotation without re-encrypting all data. Furthermore, integrating with decentralized identity standards like ERC-725 or Verifiable Credentials can provide a more standardized framework for managing keys and claims associated with a profile.

This pattern's trade-offs include on-chain gas costs for managing permissions and the need for users to be online to encrypt keys for new viewers. However, it provides a cryptographically secure, audit-friendly, and user-centric model for privacy. The public blockchain acts as a verifiable access log, while the sensitive data remains confidential, accessible only through explicit user consent encoded in the smart contract state.

step-4-retrieve-decrypt
TUTORIAL

Step 4: Retrieving and Decrypting a Profile

Learn how to fetch encrypted profile data from a public blockchain and decrypt it locally using the user's private key.

After a user's profile data is encrypted and stored on-chain, the next step is to retrieve and decrypt it. This process is initiated by a client application, such as a dApp frontend, which queries the smart contract for the encrypted data blob associated with a specific user's address. The contract returns the ciphertext, which is the encrypted data, and the ephemeral public key used during the initial encryption. This data is stored publicly on the blockchain, but it remains inaccessible without the corresponding private key.

The core of the decryption process happens off-chain, in the user's secure environment. The client application uses a cryptographic library like libsodium-wrappers or the Web Crypto API. The user must provide their private key, which is never transmitted over the network. The library uses this private key to perform a key agreement (e.g., X25519) with the ephemeral public key retrieved from the chain, deriving the same symmetric session key that was used for encryption. This session key is then used to decrypt the ciphertext.

Here is a simplified JavaScript example using a hypothetical Ethereum dApp and the tweetnacl library for decryption:

javascript
import nacl from 'tweetnacl';

async function decryptProfile(userPrivateKey, encryptedDataFromChain) {
  // encryptedDataFromChain contains: ciphertext, ephemeralPublicKey, nonce
  const { ciphertext, ephemeralPublicKey, nonce } = encryptedDataFromChain;

  // Perform key exchange to derive the shared secret
  const sharedSecret = nacl.box.before(
    new Uint8Array(ephemeralPublicKey),
    new Uint8Array(userPrivateKey)
  );

  // Decrypt the ciphertext using the shared secret and nonce
  const decryptedBytes = nacl.box.open.after(
    new Uint8Array(ciphertext),
    new Uint8Array(nonce),
    sharedSecret
  );

  if (!decryptedBytes) {
    throw new Error('Decryption failed. Invalid key or corrupted data.');
  }

  // Convert decrypted bytes back to a string (the original JSON profile)
  return new TextDecoder().decode(decryptedBytes);
}

This function takes the user's private key and the on-chain data object, performs the decryption, and returns the original plaintext JSON string.

Handling decryption failures is critical. If the private key is incorrect, the derived shared secret will not match, and nacl.box.open.after() will return null. Your application should handle this gracefully, typically by prompting the user to ensure they are using the correct wallet or key pair. It's also important to validate the decrypted data structure before using it in your application to guard against malformed data.

The security model here is robust: the blockchain acts as a public, immutable data availability layer, while the encryption/decryption keys remain solely with the user. This pattern, often called client-side encryption, is fundamental for building privacy-preserving applications on public ledgers. It enables user-controlled data without exposing sensitive information to the network or the smart contract logic itself.

ENCRYPTED PROFILES

Frequently Asked Questions

Common technical questions and solutions for developers implementing encrypted user profiles on public blockchains like Ethereum.

Encrypted user profiles store personal data (like a username, bio, or preferences) in an encrypted format on a public blockchain. The core data is stored off-chain, typically on decentralized storage like IPFS or Arweave, with only the encrypted content identifier (CID) and access control logic written on-chain.

Key reasons for using a public blockchain:

  • Immutable Access Rules: Smart contracts enforce who can decrypt data, preventing unilateral changes by a central party.
  • User Sovereignty: Users hold their own decryption keys, not a centralized service.
  • Composability: Other dApps can permissionedly read a user's verified public profile data.

This architecture separates the durable, verifiable ledger (the blockchain) from the bulk data storage, optimizing for cost and scalability while maintaining user control.

conclusion
KEY TAKEAWAYS

Conclusion and Next Steps

This guide has walked through the core concepts and implementation steps for creating encrypted user profiles on a public blockchain. You should now understand the fundamental architecture and have a functional prototype.

Building encrypted profiles requires a clear separation of on-chain and off-chain data. The public blockchain, like Ethereum or Polygon, acts as an immutable registry for user identifiers and pointers to encrypted data. The actual profile data—such as a bio, preferences, or contact details—is encrypted using the user's public key and stored off-chain in a decentralized storage network like IPFS or Arweave. This hybrid approach balances transparency with privacy, ensuring sensitive information is never exposed on the public ledger.

The security of this system hinges on proper key management. Users must safeguard their private keys, as losing them means permanent loss of access to their encrypted data. For a production application, consider integrating with wallet providers for seamless key handling or exploring social recovery mechanisms like those in ERC-4337 smart accounts. Always use established, audited libraries for cryptographic operations, such as ethers.js or libsodium, to avoid introducing vulnerabilities in your encryption logic.

To extend this basic setup, you can implement several advanced features. Add granular access control by encrypting specific profile fields with different keys, allowing selective sharing. Integrate zero-knowledge proofs (ZKPs) to allow users to prove attributes (like being over 18) without revealing the underlying data. You can also use the on-chain identifier as a universal username across different dApps, creating a portable, private identity layer for the decentralized web.

For further learning, explore related standards and tools. The ERC-725 and ERC-735 standards define a framework for on-chain identity and claim management. The Ceramic Network provides a protocol for creating and managing mutable, decentralized data streams. Reviewing the source code for identity-focused projects like ENS (Ethereum Name Service) or BrightID can provide valuable architectural insights.

Your next practical step is to audit and test your implementation thoroughly. Write comprehensive unit tests for your smart contracts and encryption functions. Conduct a security review, focusing on key storage, encryption algorithm choices, and access control logic. Finally, consider the user experience: how will you guide users through backing up their keys and understanding what data is stored where? A secure system is only effective if it's usable.