Skip to content

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.

Notifications You must be signed in to change notification settings

anoop04singh/SecureID

Repository files navigation

SecureID: Decentralized Identity Verification System

Table of Contents

  1. Introduction
  2. System Architecture
  3. Identity Creation Process
  4. Dashboard and Identity Management
  5. Verification Process
  6. Smart Contract Implementation
  7. Cryptographic Security Features
  8. Privacy Considerations
  9. Technical Components
  10. Future Enhancements

Introduction

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

System Architecture

The system consists of three main components:

  1. Frontend Application: A Next.js web application that provides the user interface for creating, managing, and verifying identities.
  2. Smart Contract: An Ethereum smart contract (IdentityVerifier.sol) deployed on the Sepolia testnet that stores identity proofs and handles verification.
  3. Cryptographic Libraries: Client-side libraries for QR code processing, zero-knowledge proof generation, and cryptographic operations.

Data Flow

  1. Users scan their ID card containing a QR code
  2. The QR code data is processed locally (never sent to a server)
  3. Zero-knowledge proofs are generated from the identity data
  4. Only the proofs and public signals (not the actual data) are stored on the blockchain
  5. Verification is performed by validating the proofs without revealing the underlying data

Identity Creation Process

The identity creation process is implemented in app/create/page.tsx and consists of several carefully designed steps to ensure security and privacy.

Step 1: ID Card Scanning

// 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:

  1. Accesses the device camera using the getUserMedia API
  2. Creates a video element to display the camera feed
  3. Captures frames from the video and draws them to a canvas
  4. Processes each frame to detect QR codes
  5. 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:

  1. Converting the QR data from base10 to a byte array
  2. Decompressing the data if it's compressed
  3. Parsing the byte array according to the ID card specification
  4. Extracting fields like name, date of birth, gender, etc.
  5. 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.

Step 2: Document Verification

// 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:

  1. The user actually has the physical ID card in their possession
  2. 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:

  1. Hashes the document reference ID using a one-way hash function
  2. Calls the isDocumentUsed function on the smart contract
  3. 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)

Step 3: Liveness Detection

// 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:

  1. The person creating the identity is a real, live human (not a photo or recording)
  2. 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.

Step 4: Zero-Knowledge Proof Generation

// 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:

  1. The identity data is valid
  2. The person is of a certain age (over/under 18)
  3. 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.

Step 5: Storing Proof on Blockchain

// 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:

  1. Creates a contract instance using the signer
  2. Prepares the proof data, including liveness verification status
  3. Hashes the document reference ID to prevent reuse
  4. Calls the storeProof function on the smart contract
  5. 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.

Dashboard and Identity Management

The dashboard (app/dashboard/page.tsx) provides users with a central interface to manage their identity and generate verification QR codes.

Identity Information Display

// 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:

  1. Creates a contract instance using the signer
  2. Gets the user's address
  3. Calls the getUserIdentity function on the smart contract
  4. 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

Verification Code Generation

// 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:

  1. A random 6-digit code is generated using generateVerificationCode
  2. The code is hashed with the user's address using hashVerificationCode
  3. The user's address is hashed using hashAddress
  4. The code has a 5-minute expiry time for security
  5. 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

QR Code Generation

// 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:

  1. Identity QR: For general identity verification
  2. 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:

  1. Converts the data to a JSON string
  2. Generates a QR code as a data URL
  3. Returns the data URL for display

Identity Deletion

// 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:

  1. Creates a contract instance using the signer
  2. Calls the deleteIdentity function on the smart contract
  3. 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

Verification Process

The verification process (app/verify/page.tsx) allows third parties to verify a user's identity or age without accessing their personal information.

Verification Code Entry

// 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:

  1. Ensures that the user has consented to the verification
  2. Prevents unauthorized scanning of QR codes
  3. Adds a time-limited component to the verification process
  4. Creates a cryptographic binding between the code and the verification request

QR Code Scanning

// 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:

  1. Parses the JSON data from the QR code
  2. Validates that the required fields are present
  3. Passes the data to the parent component for verification

Proof 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:

  1. Creates a contract instance using the signer
  2. Formats the address hash and code hash as bytes32
  3. Calls the verifyProofWithCodeHash function on the smart contract
  4. Returns a result object with verification status and message

The verification process is secure because:

  1. The verification code must match the code hash in the QR code
  2. The code hash is bound to the user's address
  3. The proof ID must exist on the blockchain
  4. The verification is performed by the smart contract, not by client-side code
  5. No personal information is revealed during the verification process

Verification Result Display

// 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.

Smart Contract Implementation

The IdentityVerifier.sol contract is the backbone of the system, handling the storage and verification of identity proofs.

Data Structures

// 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 data
  • proofData: Maps proof IDs to the actual proof data
  • documentHashes: Maps document hashes to a boolean indicating if they've been used
  • addressHashes: Maps address hashes to the original addresses

This structure ensures that:

  1. Each user can have only one active identity
  2. Each document can be used only once
  3. Proofs can be verified using the proof ID
  4. Addresses can be verified using their hash

Storing Proofs

// 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:

  1. Ensures the proof ID is not empty
  2. Ensures the user doesn't already have an active identity
  3. Ensures the document hasn't been used before

If all checks pass, it:

  1. Stores the identity in the userIdentities mapping
  2. Stores the proof data in the proofData mapping
  3. Marks the document as used in the documentHashes mapping
  4. Stores the address hash in the addressHashes mapping
  5. Emits a ProofStored event

The function uses require statements to enforce these constraints, which will revert the transaction if any condition is not met.

Updating Liveness Status

// 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:

  1. Ensures the user has an active identity
  2. Updates the liveness status in the identity
  3. Updates the proof data to include the new liveness status
  4. Emits a ProofStored event with the updated information

This function enables users to enhance their identity security after initial creation.

Verification Functions

// 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:

  1. Computes a hash from the verification code and user address
  2. Compares the computed hash with the provided code hash
  3. Returns true if they match, false otherwise

The verifyProofWithCodeHash function verifies an identity proof using the verification code and code hash. It:

  1. Gets the user's address from the address hash
  2. Gets the user's identity from the userIdentities mapping
  3. Checks if the identity exists and is not deleted
  4. Checks if the proof IDs match
  5. Verifies the code hash using the verifyCodeHash function
  6. Returns true if both checks pass, false otherwise

These functions enable secure verification without revealing the user's address or personal information.

Document Usage Checking

// 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:

  1. Takes a document hash as input
  2. Returns the boolean value from the documentHashes mapping

This function is used during identity creation to prevent document reuse.

Identity Deletion

// 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:

  1. Ensures the user has an active identity
  2. Marks the identity as deleted in the userIdentities mapping
  3. Emits a IdentityDeleted event

Importantly, the document hash remains in the documentHashes mapping to prevent reuse, even after deletion.

Cryptographic Security Features

One-Way Hashing

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:

  1. Verification codes combined with addresses
  2. Wallet addresses
  3. 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

Zero-Knowledge Proofs

// 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:

  1. Creates a cryptographic commitment to the identity data using the Poseidon hash function
  2. Generates a unique proof ID
  3. Creates public signals (isAdult, livenessVerified) that can be shared
  4. 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

Privacy Considerations

Wallet Address Protection

// 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:

  1. Hashing the address before including it in QR codes
  2. Storing the mapping between address hashes and addresses on the blockchain
  3. 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

Document ID Protection

// 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:

  1. Hashing the document reference ID before sending it to the blockchain
  2. Storing only the hash in the documentHashes mapping
  3. 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

Personal Data Protection

The system protects personal data by:

  1. Processing all personal data client-side (never sending it to a server)
  2. Generating zero-knowledge proofs that verify claims without revealing data
  3. Storing only cryptographic commitments and proofs on the blockchain
  4. 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

Technical Components

QR Code Processing

The system uses several components for QR code processing:

  1. QR Scanner (components/qr-scanner.tsx): Handles scanning QR codes from images or camera feeds
  2. SecureQRDecoder (lib/secure-qr-decoder.ts): Decodes and processes QR code data from ID cards
  3. QR Generator (lib/qr-generator.ts): Generates QR codes for identity verification

These components work together to:

  1. Extract identity information from ID card QR codes
  2. Generate verification QR codes that protect privacy
  3. Scan and verify QR codes during the verification process

Liveness Detection

The system implements several approaches to liveness detection:

  1. NoCameraVerification (components/no-camera-verification.tsx): A fallback approach that uses challenge-response verification
  2. SimpleLivenessDetection (components/simple-liveness-detection.tsx): A basic approach using visual challenges
  3. 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.

Smart Contract Interaction

The system interacts with the smart contract through several functions in lib/contract-interactions.ts:

  1. storeIdentityProof: Stores a new identity proof on the blockchain
  2. isDocumentUsed: Checks if a document has been used before
  3. updateLivenessStatus: Updates the liveness verification status
  4. getUserIdentity: Retrieves a user's identity information
  5. verifyIdentityProofWithCodeHash: Verifies an identity proof using a verification code
  6. deleteIdentity: 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.

Future Enhancements

The system could be enhanced in several ways:

  1. Real Zero-Knowledge Proofs: Implement actual zero-knowledge proofs using libraries like circom or snarkjs
  2. Multi-Factor Authentication: Add additional verification methods for enhanced security
  3. Decentralized Storage: Use IPFS or similar for storing proof data off-chain
  4. Credential Issuance: Enable issuance of verifiable credentials based on verified identities
  5. Selective Disclosure: Allow users to selectively disclose specific attributes (e.g., over 21, resident of a specific country)
  6. Integration with DeFi: Enable KYC/AML compliance for decentralized finance applications
  7. 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.

About

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.

Topics

Resources

Stars

Watchers

Forks