- Introduction
- System Architecture
- Identity Creation Process
- Dashboard and Identity Management
- Verification Process
- Smart Contract Implementation
- Cryptographic Security Features
- Privacy Considerations
- Technical Components
- Future Enhancements
SecureID is a decentralized identity verification system built on blockchain technology that enables users to create, manage, and verify identities without revealing sensitive personal information. The system leverages zero-knowledge proofs, cryptographic hashing, and blockchain technology to provide a secure, private, and tamper-proof identity verification solution.
The core principles of the system are:
- Privacy by design: Personal data is never stored on-chain
- Self-sovereignty: Users control their own identity data
- Selective disclosure: Users choose what information to share
- Cryptographic security: Strong cryptographic primitives ensure data integrity
- Decentralization: No central authority controls the identity verification process
The system consists of three main components:
- Frontend Application: A Next.js web application that provides the user interface for creating, managing, and verifying identities.
- Smart Contract: An Ethereum smart contract (
IdentityVerifier.sol
) deployed on the Sepolia testnet that stores identity proofs and handles verification. - Cryptographic Libraries: Client-side libraries for QR code processing, zero-knowledge proof generation, and cryptographic operations.
- Users scan their ID card containing a QR code
- The QR code data is processed locally (never sent to a server)
- Zero-knowledge proofs are generated from the identity data
- Only the proofs and public signals (not the actual data) are stored on the blockchain
- Verification is performed by validating the proofs without revealing the underlying data
The identity creation process is implemented in app/create/page.tsx
and consists of several carefully designed steps to ensure security and privacy.
// From components/qr-scanner.tsx
const handleQrCodeScanned = async (data: any) => {
setIsProcessing(true)
setError(null)
try {
console.log("QR code scanned successfully:", data)
// The data is already processed by the SecureQRDecoder
setQrData(data)
setStep(2)
// ...
} catch (error: any) {
// Error handling...
} finally {
setIsProcessing(false)
}
}
The QR scanner component (components/qr-scanner.tsx
) provides two methods for scanning:
- Upload: Users can upload an image containing a QR code
- Camera: Users can use their device's camera to scan a QR code in real-time
The scanner uses the jsQR
library to detect and decode QR codes. For camera-based scanning, the component:
- Accesses the device camera using the
getUserMedia
API - Creates a video element to display the camera feed
- Captures frames from the video and draws them to a canvas
- Processes each frame to detect QR codes
- When a QR code is detected, it decodes the data and passes it to the parent component
The QR code on the ID card contains encrypted personal information. The SecureQRDecoder
class (lib/secure-qr-decoder.ts
) handles the complex process of:
- Converting the QR data from base10 to a byte array
- Decompressing the data if it's compressed
- Parsing the byte array according to the ID card specification
- Extracting fields like name, date of birth, gender, etc.
- Calculating the person's age based on their date of birth
All of this processing happens entirely client-side, ensuring that sensitive personal data never leaves the user's device.
// From app/create/page.tsx
const verifyLastFourDigits = async () => {
// ...
// Get the first 4 digits of the reference ID from QR data
const firstFourDigits = qrData.referenceId.toString().substring(0, 4)
if (lastFourDigits === firstFourDigits) {
// Check if this document has already been used
if (signer) {
setIsCheckingDocument(true)
try {
const documentUsed = await isDocumentUsed(signer, qrData.referenceId)
if (documentUsed) {
setError("This ID card has already been used to create an identity. Each ID can only be used once.")
// ...
return
}
} catch (error) {
// Error handling...
} finally {
setIsCheckingDocument(false)
}
}
// ...
} else {
setError("The digits you entered do not match our records.")
// ...
}
}
After scanning, the user must enter the first four digits of their ID's reference number. This serves as a proof-of-possession check to verify that:
- The user actually has the physical ID card in their possession
- The user knows information about the ID that isn't immediately visible in the QR code
The system also checks if the document has already been used by calling the isDocumentUsed
function, which interacts with the smart contract:
// From lib/contract-interactions.ts
export async function isDocumentUsed(signer: ethers.JsonRpcSigner, documentReferenceId: string): Promise<boolean> {
try {
const contract = new ethers.Contract(CONTRACT_ADDRESS, IdentityVerifierABI, signer)
// Hash the document reference ID
const documentHash = hashDocumentId(documentReferenceId)
// Check if the document has been used
const isUsed = await contract.isDocumentUsed(documentHash)
return isUsed
} catch (error) {
// Error handling...
}
}
This function:
- Hashes the document reference ID using a one-way hash function
- Calls the
isDocumentUsed
function on the smart contract - Returns a boolean indicating whether the document has been used before
The document reference ID is never stored directly on the blockchain. Instead, only its hash is stored, which provides several security benefits:
- The original ID cannot be derived from the hash (one-way function)
- The same document cannot be used twice (prevents duplicate identities)
- The actual document ID remains private (privacy preservation)
// From app/create/page.tsx
const handleLivenessComplete = (success: boolean) => {
if (success) {
setLivenessVerified(true)
toast({
title: "Liveness Check Passed",
description: "Your identity has been verified successfully.",
})
} else {
// Allow proceeding even if liveness check is skipped
toast({
title: "Liveness Check Skipped",
description: "You can still proceed, but your identity will have a lower trust score.",
})
}
// Move to the next step
setStep(3)
}
The liveness detection step (components/no-camera-verification.tsx
) is crucial for preventing identity fraud. It verifies that:
- The person creating the identity is a real, live human (not a photo or recording)
- The person is physically present during the identity creation process
The liveness detection component implements several verification methods:
- Math captcha: The user must solve a simple math problem
- Pattern memorization: The user is shown a pattern they must remember and reproduce
- Device motion detection: The system detects if the device is being held by a human
This multi-modal approach provides robust verification while accommodating different device capabilities. The liveness verification status is stored as part of the identity proof, enhancing the trustworthiness of the identity.
// From app/create/page.tsx
const generateProof = async () => {
if (!identityData) return
setIsProcessing(true)
setError(null)
try {
// Add liveness verification status to the identity data
const identityWithLiveness = {
...identityData,
livenessVerified: livenessVerified,
}
// Generate zero-knowledge proof
const proof = await generateZkProof(identityWithLiveness)
// Add liveness verification status to the proof
proof.publicSignals.livenessVerified = livenessVerified
setZkProof(proof)
setProofGenerated(true)
// ...
} catch (error: any) {
// Error handling...
} finally {
setIsProcessing(false)
}
}
The zero-knowledge proof generation is a critical component of the system. The generateZkProof
function (lib/zk-proofs.ts
) creates a cryptographic proof that:
- The identity data is valid
- The person is of a certain age (over/under 18)
- The person has passed liveness verification (if applicable)
The proof structure includes:
- proofId: A unique identifier for the proof
- commitment: A cryptographic commitment to the identity data
- publicSignals: Public information that can be shared (isAdult, livenessVerified)
- proof: The actual zero-knowledge proof data
The commitment is generated using the Poseidon hash function, which is particularly efficient for zero-knowledge proofs. The proof itself contains cryptographic elements (pi_a, pi_b, pi_c) that allow verification without revealing the underlying data.
This approach allows the system to verify claims about the identity (e.g., "this person is over 18") without revealing the actual age or other personal information.
// From app/create/page.tsx
const storeProofOnChain = async () => {
if (!zkProof || !signer || !identityData || !identityData.referenceId) return
setIsProcessing(true)
setError(null)
try {
// Store proof on blockchain with document reference ID
const tx = await storeIdentityProof(signer, zkProof, identityData.referenceId)
// ...
} catch (error: any) {
// Error handling...
} finally {
setIsProcessing(false)
}
}
The final step is storing the proof on the blockchain using the storeIdentityProof
function (lib/contract-interactions.ts
):
export async function storeIdentityProof(
signer: ethers.JsonRpcSigner,
proof: any,
documentReferenceId: string,
): Promise<ethers.TransactionResponse> {
try {
const contract = new ethers.Contract(CONTRACT_ADDRESS, IdentityVerifierABI, signer)
// Include liveness verification status in the proof data
const proofData = {
...proof.proof,
livenessVerified: proof.publicSignals.livenessVerified || false,
}
// Hash the document reference ID to prevent reuse
const documentHash = hashDocumentId(documentReferenceId)
// Store the proof on-chain
const tx = await contract.storeProof(
proof.proofId,
proof.commitment,
proof.publicSignals.isAdult,
proof.publicSignals.livenessVerified || false,
JSON.stringify(proofData),
documentHash,
)
return tx
} catch (error) {
// Error handling...
}
}
This function:
- Creates a contract instance using the signer
- Prepares the proof data, including liveness verification status
- Hashes the document reference ID to prevent reuse
- Calls the
storeProof
function on the smart contract - Returns the transaction response
The smart contract stores:
- The proof ID
- The cryptographic commitment
- Public signals (isAdult, livenessVerified)
- The proof data (as a JSON string)
- The document hash (to prevent reuse)
Importantly, no personal information is stored on the blockchain, only cryptographic proofs and commitments.
The dashboard (app/dashboard/page.tsx
) provides users with a central interface to manage their identity and generate verification QR codes.
// From app/dashboard/page.tsx
const loadIdentityData = async () => {
if (!signer) return
setIsLoading(true)
try {
const identity = await getUserIdentity(signer)
setIdentityData(identity)
// ...
} catch (error) {
// Error handling...
} finally {
setIsLoading(false)
}
}
The dashboard retrieves the user's identity information from the blockchain using the getUserIdentity
function (lib/contract-interactions.ts
). This function:
- Creates a contract instance using the signer
- Gets the user's address
- Calls the
getUserIdentity
function on the smart contract - Processes the returned data into a structured format
The dashboard displays:
- Identity verification status (basic or liveness verified)
- Options to enhance security through liveness verification
- Tools to generate verification codes and QR codes
// From app/dashboard/page.tsx
const handleGenerateVerificationCode = async () => {
if (!address) return
setIsGeneratingCode(true)
try {
// Generate a random verification code locally
const code = generateVerificationCode()
// Hash the code with the user's address
const hash = hashVerificationCode(code, address)
// Hash the address for QR code
const addrHash = hashAddress(address)
// Set state
setVerificationCode(code)
setCodeHash(hash)
setAddressHash(addrHash)
// Set expiry time to 5 minutes from now
const expiry = new Date()
expiry.setMinutes(expiry.getMinutes() + 5)
setCodeExpiry(expiry)
// Generate QR codes
if (identityData) {
await generateQrCodes(identityData, hash, addrHash)
}
} catch (error: any) {
// Error handling...
} finally {
setIsGeneratingCode(false)
}
}
The verification code generation process is a critical security feature:
- A random 6-digit code is generated using
generateVerificationCode
- The code is hashed with the user's address using
hashVerificationCode
- The user's address is hashed using
hashAddress
- The code has a 5-minute expiry time for security
- QR codes are generated containing the hashes (not the code itself)
This approach provides several security benefits:
- The verification code is time-limited (expires after 5 minutes)
- The code must be shared out-of-band with the verifier
- The QR code doesn't contain the actual verification code, only its hash
- The user's wallet address is never exposed, only its hash
// From app/dashboard/page.tsx
const generateQrCodes = async (identity: any, codeHash: string, addrHash: string) => {
try {
// Generate QR codes with the address hash and code hash only
const identityQrCode = await generateQrCode({
type: "identity",
proofId: identity.proofId,
addressHash: addrHash,
codeHash: codeHash,
})
const ageQrCode = await generateQrCode({
type: "age",
proofId: identity.proofId,
addressHash: addrHash,
codeHash: codeHash,
})
setIdentityQr(identityQrCode)
setAgeQr(ageQrCode)
setShowQrCodes(true)
} catch (error) {
// Error handling...
}
}
The dashboard generates two types of QR codes:
- Identity QR: For general identity verification
- Age QR: Specifically for age verification (over/under 18)
Both QR codes contain:
- The proof ID (to identify the proof on the blockchain)
- The address hash (to identify the user without revealing their address)
- The code hash (to verify the verification code without revealing it)
- The type of verification ("identity" or "age")
The QR codes are generated using the generateQrCode
function (lib/qr-generator.ts
), which:
- Converts the data to a JSON string
- Generates a QR code as a data URL
- Returns the data URL for display
// From app/dashboard/page.tsx
const handleDeleteIdentity = async () => {
if (!signer) return
if (deleteConfirmation !== address) {
toast({
title: "Confirmation Failed",
description: "The wallet address you entered doesn't match your current address.",
variant: "destructive",
})
return
}
setIsDeleting(true)
try {
const tx = await deleteIdentity(signer)
await tx.wait()
// ...
} catch (error: any) {
// Error handling...
} finally {
setIsDeleting(false)
}
}
The dashboard also provides functionality to delete an identity using the deleteIdentity
function (lib/contract-interactions.ts
). This function:
- Creates a contract instance using the signer
- Calls the
deleteIdentity
function on the smart contract - Returns the transaction response
For security, the user must confirm deletion by entering their wallet address. When an identity is deleted:
- The identity is marked as deleted in the smart contract
- The document hash remains in the contract to prevent reuse
- The user can create a new identity with a different document
The verification process (app/verify/page.tsx
) allows third parties to verify a user's identity or age without accessing their personal information.
// From app/verify/page.tsx
const handleProceedToScan = () => {
if (verificationCode.length !== 6) {
toast({
title: "Invalid Code",
description: "Please enter a valid 6-digit verification code.",
variant: "destructive",
})
return
}
setStep("scan")
toast({
title: "Verification Code Entered",
description: "Now scan the QR code to complete verification.",
})
}
The verification process begins with the verifier entering the 6-digit verification code provided by the user. This code is essential for the security of the verification process, as it:
- Ensures that the user has consented to the verification
- Prevents unauthorized scanning of QR codes
- Adds a time-limited component to the verification process
- Creates a cryptographic binding between the code and the verification request
// From app/verify/page.tsx
const handleQrCodeScanned = async (data: string) => {
if (!signer) {
toast({
title: "Wallet Not Connected",
description: "Please connect your wallet to verify identities.",
variant: "destructive",
})
return
}
setIsProcessing(true)
try {
// Parse QR data
let qrData
try {
qrData = JSON.parse(data)
} catch (error) {
throw new Error("Invalid QR code format. Please scan a valid identity verification QR code.")
}
// Validate the QR data structure
if (!qrData.proofId || !qrData.addressHash || !qrData.type || !qrData.codeHash) {
throw new Error("Invalid QR code data. Missing required verification information.")
}
// Store QR data
setQrData(qrData)
// Verify the proof using the verification code and code hash
await verifyWithCodeHash(qrData)
} catch (error: any) {
// Error handling...
} finally {
setIsProcessing(false)
}
}
The QR code scanning is handled by the VerificationQrScanner
component (components/verification-qr-scanner.tsx
), which provides similar functionality to the QR scanner used in identity creation but is specifically optimized for verification QR codes.
When a QR code is scanned, the component:
- Parses the JSON data from the QR code
- Validates that the required fields are present
- Passes the data to the parent component for verification
// From app/verify/page.tsx
const verifyWithCodeHash = async (qrData: any) => {
if (!signer) return
setIsProcessing(true)
try {
// Verify the proof with code hash
const result = await verifyIdentityProofWithCodeHash(
signer,
qrData.proofId,
qrData.addressHash,
verificationCode,
qrData.codeHash,
qrData.type,
)
if (result.verified) {
toast({
title: "Verification Successful",
description: `The ${qrData.type} has been verified successfully.`,
})
} else {
toast({
title: "Verification Failed",
description: "The proof could not be verified.",
variant: "destructive",
})
}
setVerificationResult(result)
setStep("result")
} catch (error: any) {
// Error handling...
} finally {
setIsProcessing(false)
}
}
The verification is performed using the verifyIdentityProofWithCodeHash
function (lib/contract-interactions.ts
):
export async function verifyIdentityProofWithCodeHash(
signer: ethers.JsonRpcSigner,
proofId: string,
addressHash: string,
verificationCode: string,
codeHash: string,
verificationType: string,
): Promise<{ verified: boolean; type: string; message: string }> {
try {
const contract = new ethers.Contract(CONTRACT_ADDRESS, IdentityVerifierABI, signer)
// Ensure addressHash and codeHash are properly formatted as bytes32
const formattedAddressHash = addressHash.startsWith("0x") ? addressHash : `0x${addressHash}`
const formattedCodeHash = codeHash.startsWith("0x") ? codeHash : `0x${codeHash}`
// Use staticCall to make a read-only call
const isValid = await contract.verifyProofWithCodeHash.staticCall(
proofId,
formattedAddressHash,
verificationCode,
formattedCodeHash,
)
if (!isValid) {
return {
verified: false,
type: verificationType,
message: "The proof could not be verified...",
}
}
// Return success message based on verification type
if (verificationType === "age") {
return {
verified: true,
type: "age",
message: "The person is verified to be over 18 years old.",
}
} else {
return {
verified: true,
type: "identity",
message: "The identity has been successfully verified.",
}
}
} catch (error) {
// Error handling...
}
}
This function:
- Creates a contract instance using the signer
- Formats the address hash and code hash as bytes32
- Calls the
verifyProofWithCodeHash
function on the smart contract - Returns a result object with verification status and message
The verification process is secure because:
- The verification code must match the code hash in the QR code
- The code hash is bound to the user's address
- The proof ID must exist on the blockchain
- The verification is performed by the smart contract, not by client-side code
- No personal information is revealed during the verification process
// From app/verify/page.tsx
{step === "result" && (
<div className="space-y-4">
<div className="flex justify-center py-4">
{verificationResult?.verified ? (
<CheckCircle2 className="h-16 w-16 text-green-500" />
) : (
<XCircle className="h-16 w-16 text-red-500" />
)}
</div>
<Alert
className={verificationResult?.verified ? "bg-green-50 border-green-200" : "bg-red-50 border-red-200"}
>
<AlertTitle>
{verificationResult?.verified ? "Verification Successful" : "Verification Failed"}
</AlertTitle>
<AlertDescription>{verificationResult?.message}</AlertDescription>
</Alert>
{verificationResult?.verified && verificationResult?.type === "identity" && (
<div className="text-sm text-center text-muted-foreground">
The identity has been verified without revealing any personal information.
</div>
)}
{verificationResult?.verified && verificationResult?.type === "age" && (
<div className="text-sm text-center text-muted-foreground">
The person is verified to be over 18 years old without revealing their actual age.
</div>
)}
</div>
)}
After verification, the result is displayed to the user with:
- A visual indicator (green check or red X)
- A message explaining the verification result
- Additional context based on the verification type
For successful verifications, the system confirms that:
- For identity verification: The identity is valid
- For age verification: The person is over/under 18
In both cases, the system emphasizes that no personal information was revealed during the verification process.
The IdentityVerifier.sol
contract is the backbone of the system, handling the storage and verification of identity proofs.
// From contracts/IdentityVerifier.sol
struct Identity {
string proofId;
string commitment;
bool isAdult;
bool livenessVerified;
uint256 timestamp;
bool isDeleted;
}
// Mapping from user address to their identity
mapping(address => Identity) private userIdentities;
// Mapping from proofId to proof data
mapping(string => string) private proofData;
// Mapping from document hash to boolean (used to prevent document reuse)
mapping(bytes32 => bool) private documentHashes;
// Mapping from address hash to address (used for QR code verification)
mapping(bytes32 => address) private addressHashes;
The contract uses several mappings to store and organize data:
userIdentities
: Maps user addresses to their identity dataproofData
: Maps proof IDs to the actual proof datadocumentHashes
: Maps document hashes to a boolean indicating if they've been usedaddressHashes
: Maps address hashes to the original addresses
This structure ensures that:
- Each user can have only one active identity
- Each document can be used only once
- Proofs can be verified using the proof ID
- Addresses can be verified using their hash
// From contracts/IdentityVerifier.sol
function storeProof(
string calldata proofId,
string calldata commitment,
bool isAdult,
bool livenessVerified,
string calldata proofDataStr,
bytes32 documentHash
) external {
// Ensure the proof ID is not empty
require(bytes(proofId).length > 0, "Proof ID cannot be empty");
// Ensure user doesn't already have an active identity
require(bytes(userIdentities[msg.sender].proofId).length == 0 ||
userIdentities[msg.sender].isDeleted,
"User already has an active identity");
// Ensure document hasn't been used before
require(!documentHashes[documentHash], "This document has already been used to create an identity");
// Store the identity
userIdentities[msg.sender] = Identity({
proofId: proofId,
commitment: commitment,
isAdult: isAdult,
livenessVerified: livenessVerified,
timestamp: block.timestamp,
isDeleted: false
});
// Store the proof data
proofData[proofId] = proofDataStr;
// Mark document as used
documentHashes[documentHash] = true;
// Store address hash for QR verification
bytes32 addressHash = keccak256(abi.encodePacked(msg.sender));
addressHashes[addressHash] = msg.sender;
// Emit event
emit ProofStored(proofId, msg.sender, isAdult, livenessVerified, block.timestamp);
}
The storeProof
function is responsible for storing a new identity proof. It performs several important checks:
- Ensures the proof ID is not empty
- Ensures the user doesn't already have an active identity
- Ensures the document hasn't been used before
If all checks pass, it:
- Stores the identity in the
userIdentities
mapping - Stores the proof data in the
proofData
mapping - Marks the document as used in the
documentHashes
mapping - Stores the address hash in the
addressHashes
mapping - Emits a
ProofStored
event
The function uses require
statements to enforce these constraints, which will revert the transaction if any condition is not met.
// From contracts/IdentityVerifier.sol
function updateLivenessStatus(bool livenessVerified) external {
// Ensure the user has an identity
require(bytes(userIdentities[msg.sender].proofId).length > 0, "No identity found for this user");
require(!userIdentities[msg.sender].isDeleted, "Identity has been deleted");
// Update liveness status
userIdentities[msg.sender].livenessVerified = livenessVerified;
// Update proof data to include liveness verification
string memory proofId = userIdentities[msg.sender].proofId;
string memory proofDataStr = proofData[proofId];
// In a real implementation, we would update the ZK proof data here
// For this demo, we'll just append the liveness status
proofData[proofId] = string(abi.encodePacked(proofDataStr, ", \"livenessVerified\": ", livenessVerified ? "true" : "false"));
// Emit event
emit ProofStored(proofId, msg.sender, userIdentities[msg.sender].isAdult, livenessVerified, block.timestamp);
}
The updateLivenessStatus
function allows users to update their liveness verification status. It:
- Ensures the user has an active identity
- Updates the liveness status in the identity
- Updates the proof data to include the new liveness status
- Emits a
ProofStored
event with the updated information
This function enables users to enhance their identity security after initial creation.
// From contracts/IdentityVerifier.sol
function verifyCodeHash(string calldata verificationCode, bytes32 codeHash, address userAddress) public pure returns (bool) {
bytes32 computedHash = keccak256(abi.encodePacked(verificationCode, ":", userAddress));
return computedHash == codeHash;
}
function verifyProofWithCodeHash(
string calldata proofId,
bytes32 addressHash,
string calldata verificationCode,
bytes32 codeHash
) external view returns (bool) {
// Get the user's address from the address hash
address userAddress = getAddressFromHash(addressHash);
// Get the user's identity
Identity memory identity = userIdentities[userAddress];
// Check if the identity exists and is not deleted
require(bytes(identity.proofId).length > 0, "No identity found for this user");
require(!identity.isDeleted, "Identity has been deleted");
// Check if the proof IDs match
bool proofMatches = keccak256(bytes(identity.proofId)) == keccak256(bytes(proofId));
// Verify the code hash
bool codeValid = verifyCodeHash(verificationCode, codeHash, userAddress);
// In a real implementation, we would verify the ZK proof here
return proofMatches && codeValid;
}
The verifyCodeHash
function checks if a verification code matches a code hash for a specific user address. It:
- Computes a hash from the verification code and user address
- Compares the computed hash with the provided code hash
- Returns true if they match, false otherwise
The verifyProofWithCodeHash
function verifies an identity proof using the verification code and code hash. It:
- Gets the user's address from the address hash
- Gets the user's identity from the
userIdentities
mapping - Checks if the identity exists and is not deleted
- Checks if the proof IDs match
- Verifies the code hash using the
verifyCodeHash
function - Returns true if both checks pass, false otherwise
These functions enable secure verification without revealing the user's address or personal information.
// From contracts/IdentityVerifier.sol
function isDocumentUsed(bytes32 documentHash) external view returns (bool) {
return documentHashes[documentHash];
}
The isDocumentUsed
function checks if a document has been used before. It:
- Takes a document hash as input
- Returns the boolean value from the
documentHashes
mapping
This function is used during identity creation to prevent document reuse.
// From contracts/IdentityVerifier.sol
function deleteIdentity() external {
// Ensure the user has an identity
require(bytes(userIdentities[msg.sender].proofId).length > 0, "No identity found for this user");
require(!userIdentities[msg.sender].isDeleted, "Identity already deleted");
// Mark the identity as deleted
userIdentities[msg.sender].isDeleted = true;
// Note: We don't remove the document hash from documentHashes
// This ensures the document can't be reused even after deletion
// Emit event
emit IdentityDeleted(msg.sender, block.timestamp);
}
The deleteIdentity
function allows users to delete their identity. It:
- Ensures the user has an active identity
- Marks the identity as deleted in the
userIdentities
mapping - Emits a
IdentityDeleted
event
Importantly, the document hash remains in the documentHashes
mapping to prevent reuse, even after deletion.
The system uses one-way cryptographic hash functions extensively to protect sensitive information:
// From lib/verification-utils.ts
export function hashVerificationCode(code: string, address: string): string {
// First pack the data with the correct types
const packed = ethers.solidityPacked(["string", "string", "address"], [code, ":", address])
// Then hash the packed data
const hash = ethers.keccak256(packed)
// Return without 0x prefix for QR code
return hash
}
export function hashAddress(address: string): string {
// Use solidityPack with "address" type to ensure correct encoding
const packed = ethers.solidityPacked(["address"], [address])
// Then hash the packed data
const hash = ethers.keccak256(packed)
// Return without 0x prefix for QR code
return hash
}
These functions use the keccak256 hash function (the same used in Ethereum) to create one-way hashes of:
- Verification codes combined with addresses
- Wallet addresses
- Document reference IDs
One-way hashing provides several security benefits:
- Irreversibility: It's computationally infeasible to derive the original input from the hash
- Determinism: The same input always produces the same hash
- Uniqueness: Different inputs (almost) always produce different hashes
- Efficiency: Hashing is fast to compute
These properties make one-way hashing ideal for:
- Storing document IDs without revealing them
- Verifying codes without exposing them
- Identifying users without revealing their addresses
// From lib/zk-proofs.ts
export async function generateZkProof(data: IdentityData): Promise<any> {
try {
// Use Poseidon hash for commitment
const poseidon = await buildPoseidon()
// Create a commitment to the identity data
const uid = data.referenceId || hashString(data.name || "unknown").toString()
const age = data.age || 0
const isAdult = data.isAdult || false
const livenessVerified = data.livenessVerified || false
// Include liveness verification in the commitment
const commitment = poseidon.F.toString(
poseidon([BigInt(hashString(uid)), BigInt(age), isAdult ? 1n : 0n, livenessVerified ? 1n : 0n]),
)
// Generate a unique proof ID
const proofId = generateProofId(uid)
// In a real implementation, we would generate actual ZK proofs here
return {
proofId,
commitment,
publicSignals: {
isAdult: isAdult,
livenessVerified: livenessVerified,
},
// This would be the actual proof in a real implementation
proof: {
pi_a: [commitment.slice(0, 10), commitment.slice(10, 20)],
pi_b: [
[commitment.slice(20, 30), commitment.slice(30, 40)],
[commitment.slice(40, 50), commitment.slice(50, 60)],
],
pi_c: [commitment.slice(60, 70), commitment.slice(70, 80)],
},
}
} catch (error) {
// Error handling...
}
}
The system uses zero-knowledge proofs to enable verification of claims without revealing the underlying data. The generateZkProof
function:
- Creates a cryptographic commitment to the identity data using the Poseidon hash function
- Generates a unique proof ID
- Creates public signals (isAdult, livenessVerified) that can be shared
- Generates a proof structure that would contain the actual zero-knowledge proof in a production implementation
Zero-knowledge proofs provide several security benefits:
- Privacy: The prover can demonstrate knowledge of a value without revealing it
- Verifiability: The verifier can confirm the truth of a statement without learning the underlying data
- Minimal disclosure: Only the specific claims being verified are disclosed, not the underlying data
In this system, zero-knowledge proofs enable:
- Age verification without revealing the actual age
- Identity verification without revealing personal information
- Liveness verification without exposing biometric data
// From lib/verification-utils.ts
export function hashAddress(address: string): string {
// Use solidityPack with "address" type to ensure correct encoding
const packed = ethers.solidityPacked(["address"], [address])
// Then hash the packed data
const hash = ethers.keccak256(packed)
// Return without 0x prefix for QR code
return hash
}
The system protects user wallet addresses by:
- Hashing the address before including it in QR codes
- Storing the mapping between address hashes and addresses on the blockchain
- Using the address hash for verification instead of the actual address
This approach ensures that:
- The user's wallet address is never exposed in QR codes
- Verifiers cannot determine the user's wallet address
- The smart contract can still verify the identity using the address hash
// From lib/contract-interactions.ts
export function hashDocumentId(referenceId: string): string {
// Use solidityPack with "string" type
const packed = ethers.solidityPacked(["string"], [referenceId])
// Then hash the packed data
return ethers.keccak256(packed)
}
The system protects document IDs by:
- Hashing the document reference ID before sending it to the blockchain
- Storing only the hash in the
documentHashes
mapping - Using the hash to check for document reuse
This approach ensures that:
- The actual document ID is never stored on the blockchain
- The document cannot be reused to create multiple identities
- The document ID cannot be derived from the hash
The system protects personal data by:
- Processing all personal data client-side (never sending it to a server)
- Generating zero-knowledge proofs that verify claims without revealing data
- Storing only cryptographic commitments and proofs on the blockchain
- Using selective disclosure to reveal only necessary information
This approach ensures that:
- Personal information is never exposed during verification
- The blockchain contains no personal data
- Users control what information they share and with whom
The system uses several components for QR code processing:
- QR Scanner (
components/qr-scanner.tsx
): Handles scanning QR codes from images or camera feeds - SecureQRDecoder (
lib/secure-qr-decoder.ts
): Decodes and processes QR code data from ID cards - QR Generator (
lib/qr-generator.ts
): Generates QR codes for identity verification
These components work together to:
- Extract identity information from ID card QR codes
- Generate verification QR codes that protect privacy
- Scan and verify QR codes during the verification process
The system implements several approaches to liveness detection:
- NoCameraVerification (
components/no-camera-verification.tsx
): A fallback approach that uses challenge-response verification - SimpleLivenessDetection (
components/simple-liveness-detection.tsx
): A basic approach using visual challenges - RobustLivenessDetection (
components/robust-liveness-detection.tsx
): A more comprehensive approach using multiple verification methods
These components provide a flexible approach to liveness verification that can adapt to different device capabilities and security requirements.
The system interacts with the smart contract through several functions in lib/contract-interactions.ts
:
storeIdentityProof
: Stores a new identity proof on the blockchainisDocumentUsed
: Checks if a document has been used beforeupdateLivenessStatus
: Updates the liveness verification statusgetUserIdentity
: Retrieves a user's identity informationverifyIdentityProofWithCodeHash
: Verifies an identity proof using a verification codedeleteIdentity
: Deletes a user's identity
These functions use the ethers.js library to interact with the Ethereum blockchain, handling contract calls, transactions, and error handling.
The system could be enhanced in several ways:
- Real Zero-Knowledge Proofs: Implement actual zero-knowledge proofs using libraries like circom or snarkjs
- Multi-Factor Authentication: Add additional verification methods for enhanced security
- Decentralized Storage: Use IPFS or similar for storing proof data off-chain
- Credential Issuance: Enable issuance of verifiable credentials based on verified identities
- Selective Disclosure: Allow users to selectively disclose specific attributes (e.g., over 21, resident of a specific country)
- Integration with DeFi: Enable KYC/AML compliance for decentralized finance applications
- Mobile App: Develop a mobile application for easier identity management and verification
These enhancements would further improve the security, privacy, and usability of the system.