diff --git a/.gitguardian.yml b/.gitguardian.yml index 81a77acff..71514d3c5 100644 --- a/.gitguardian.yml +++ b/.gitguardian.yml @@ -1,12 +1,16 @@ -version: 2 +# GitGuardian configuration for ggshield +# This file configures which files and secrets to ignore during scanning -# Ignore specific file patterns (newer format) -ignore: +# Ignore specific file patterns +paths-ignore: # Mock certificates for testing (these are intentionally committed test data) - "**/mock_certificates/**/*.key" - "**/mock_certificates/**/*.crt" - "**/mock_certificates/**/*.pem" - "**/constants/mockCertificates.ts" + - "common/src/mock_certificates/**" + - "common/src/mock_certificates/aadhaar/mockAadhaarCert.ts" + - "common/src/utils/passports/genMockIdDoc.ts" # Test data files - "**/test/**/*.key" @@ -24,45 +28,17 @@ ignore: # Demo app test data - "**/demo-app/**/mock/**" - "**/demo-app/**/test-data/**" - -# Keep the old format for backward compatibility -exclusion_globs: - # Mock certificates for testing (these are intentionally committed test data) - - "common/src/mock_certificates/**" - - "common/src/constants/mockCertificates.ts" - "**/test-data/**" - "**/mock-data/**" - # Test files with mock certificates - - "**/test/**/*.key" - - "**/test/**/*.crt" - - "**/test/**/*.pem" - - "**/tests/**/*.key" - - "**/tests/**/*.crt" - - "**/tests/**/*.pem" - - # Demo app test data - - "**/demo-app/**/mock/**" - - "**/demo-app/**/test-data/**" - # Generated test files - "**/generated/**/*.key" - "**/generated/**/*.crt" - "**/generated/**/*.pem" # Ignore specific secret types for mock files -ignore_secrets: +secrets-ignore: - "Generic Private Key" # For mock certificate keys - "Generic Certificate" # For mock certificates - "RSA Private Key" # For mock RSA keys - "EC Private Key" # For mock EC keys - -# Advanced: Ignore based on file content patterns -ignore_patterns: - # Ignore files that contain "mock" in the path and have key/cert content - - pattern: "mock.*\\.(key|crt|pem)$" - reason: "Mock certificate files for testing" - - # Ignore TypeScript files that export mock data - - pattern: ".*mock.*\\.ts$" - reason: "Mock data export files for testing" diff --git a/.gitleaks.toml b/.gitleaks.toml index 3f929c79a..45864ca3d 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -22,7 +22,9 @@ paths = [ '''pnpm-lock.yaml''', '''Podfile.lock''', '''common/src/mock_certificates/.*''', + '''common/dist/.*/mock_certificates/.*''', '''common/src/constants/mockCertificates.ts''', + '''common/src/utils/passports/genMockIdDoc.ts''', '''Database.refactorlog''', '''vendor''', '''.*tamagui-components\.config\.cjs$''', diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index 38a55284a..4ad9c6090 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -217,5 +217,8 @@ dependencies { implementation "com.google.guava:guava:31.1-android" implementation "androidx.profileinstaller:profileinstaller:1.3.1" + implementation "androidx.activity:activity:1.9.3" + implementation "androidx.activity:activity-ktx:1.9.3" + implementation "com.google.android.play:app-update:2.1.0" } diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 9e269c724..ec75692a7 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -82,5 +82,20 @@ android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_color" tools:replace="android:resource" /> + + + + + + + + + diff --git a/app/android/app/src/main/java/com/proofofpassportapp/PhotoPickerActivity.java b/app/android/app/src/main/java/com/proofofpassportapp/PhotoPickerActivity.java new file mode 100644 index 000000000..a184d089e --- /dev/null +++ b/app/android/app/src/main/java/com/proofofpassportapp/PhotoPickerActivity.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +package com.proofofpassportapp; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.PickVisualMediaRequest; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; + +public class PhotoPickerActivity extends AppCompatActivity { + + public static final String EXTRA_SELECTED_URI = "selected_uri"; + public static final String EXTRA_ERROR_MESSAGE = "error_message"; + private static final String TAG = "PhotoPickerActivity"; + + private ActivityResultLauncher photoPickerLauncher; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Register the photo picker launcher using the recommended API + photoPickerLauncher = registerForActivityResult( + new ActivityResultContracts.PickVisualMedia(), + this::handlePhotoPickerResult + ); + + launchPhotoPicker(); + } + + private void launchPhotoPicker() { + try { + Log.d(TAG, "Launching modern PickVisualMedia photo picker"); + + // Create the request using the recommended builder pattern + PickVisualMediaRequest request = new PickVisualMediaRequest.Builder() + .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE) + .build(); + + photoPickerLauncher.launch(request); + + } catch (Exception e) { + Log.e(TAG, "Failed to launch photo picker: " + e.getMessage()); + finishWithError("Failed to launch photo picker: " + e.getMessage()); + } + } + + private void handlePhotoPickerResult(Uri selectedUri) { + if (selectedUri != null) { + Log.d(TAG, "Photo picker returned URI: " + selectedUri); + finishWithResult(selectedUri); + } else { + Log.d(TAG, "Photo picker was cancelled"); + finishWithError("Photo selection was cancelled"); + } + } + + private void finishWithResult(Uri selectedUri) { + Intent resultIntent = new Intent(); + resultIntent.putExtra(EXTRA_SELECTED_URI, selectedUri.toString()); + setResult(RESULT_OK, resultIntent); + finish(); + } + + private void finishWithError(String errorMessage) { + Intent resultIntent = new Intent(); + resultIntent.putExtra(EXTRA_ERROR_MESSAGE, errorMessage); + setResult(RESULT_CANCELED, resultIntent); + finish(); + } +} diff --git a/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerModule.java b/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerModule.java index 9f5bc9fbc..920c36f50 100644 --- a/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerModule.java +++ b/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerModule.java @@ -1,10 +1,16 @@ -// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. package com.proofofpassportapp; import android.app.Activity; import android.content.Intent; import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Build; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; @@ -14,14 +20,21 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.LifecycleEventListener; import com.blikoon.qrcodescanner.QrCodeActivity; import android.Manifest; +import com.proofofpassportapp.utils.QrCodeDetectorProcessor; +import example.jllarraz.com.passportreader.mlkit.FrameMetadata; +import java.io.InputStream; -public class QRCodeScannerModule extends ReactContextBaseJavaModule { +public class QRCodeScannerModule extends ReactContextBaseJavaModule implements LifecycleEventListener { private static final int REQUEST_CODE_QR_SCAN = 101; + private static final int REQUEST_CODE_PHOTO_PICK = 102; + private static final int REQUEST_CODE_MODERN_PHOTO_PICK = 103; private static final int PERMISSION_REQUEST_CAMERA = 1; private Promise scanPromise; + private Promise photoLibraryPromise; private final ActivityEventListener activityEventListener = new BaseActivityEventListener() { @Override @@ -36,13 +49,38 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, } scanPromise = null; } + } else if (requestCode == REQUEST_CODE_PHOTO_PICK && photoLibraryPromise != null) { + // Handle legacy photo picker result for older devices + if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { + processImageForQRCode(data.getData()); + } else { + photoLibraryPromise.reject("PHOTO_PICKER_CANCELLED", "Photo selection was cancelled"); + photoLibraryPromise = null; + } + } else if (requestCode == REQUEST_CODE_MODERN_PHOTO_PICK && photoLibraryPromise != null) { + // Handle modern photo picker result from dedicated activity + if (resultCode == Activity.RESULT_OK && data != null) { + String uriString = data.getStringExtra(PhotoPickerActivity.EXTRA_SELECTED_URI); + if (uriString != null) { + processImageForQRCode(Uri.parse(uriString)); + } else { + photoLibraryPromise.reject("PHOTO_PICKER_ERROR", "No URI returned from photo picker"); + photoLibraryPromise = null; + } + } else { + String errorMessage = data != null ? data.getStringExtra(PhotoPickerActivity.EXTRA_ERROR_MESSAGE) : "Photo selection was cancelled"; + photoLibraryPromise.reject("PHOTO_PICKER_CANCELLED", errorMessage); + photoLibraryPromise = null; + } } } }; - QRCodeScannerModule(ReactApplicationContext reactContext) { + public QRCodeScannerModule(ReactApplicationContext reactContext) { super(reactContext); reactContext.addActivityEventListener(activityEventListener); + reactContext.addLifecycleEventListener(this); + } @NonNull @@ -71,12 +109,111 @@ public void scanQRCode(Promise promise) { } } + @ReactMethod + public void scanQRCodeFromPhotoLibrary(Promise promise) { + Activity currentActivity = getCurrentActivity(); + if (currentActivity == null) { + promise.reject("ACTIVITY_DOES_NOT_EXIST", "Activity doesn't exist"); + return; + } + + photoLibraryPromise = promise; + + // we first try with the recomended approach. This should be sufficient for most devices with play service. + // It fallsback to document picker if photo-picker is not available. + try { + android.util.Log.d("QRCodeScanner", "Using recommended PickVisualMedia photo picker via dedicated activity"); + Intent intent = new Intent(currentActivity, PhotoPickerActivity.class); + currentActivity.startActivityForResult(intent, REQUEST_CODE_MODERN_PHOTO_PICK); + return; + } catch (Exception e) { + android.util.Log.d("QRCodeScanner", "Modern photo picker activity failed: " + e.getMessage()); + } + + // Fallback to intent-based photo picker for Android 13+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + try { + android.util.Log.d("QRCodeScanner", "Using intent-based modern photo picker (Android 13+)"); + Intent intent = new Intent("android.provider.action.PICK_IMAGES"); + intent.setType("image/*"); + currentActivity.startActivityForResult(intent, REQUEST_CODE_PHOTO_PICK); + return; + } catch (Exception e) { + android.util.Log.d("QRCodeScanner", "Intent-based modern photo picker failed: " + e.getMessage()); + } + } + + // Final fallback to legacy photo picker + android.util.Log.d("QRCodeScanner", "Using legacy Intent.ACTION_PICK photo picker"); + Intent intent = new Intent(Intent.ACTION_PICK); + intent.setType("image/*"); + currentActivity.startActivityForResult(intent, REQUEST_CODE_PHOTO_PICK); + } + private void startQRScanner(Activity activity) { Intent intent = new Intent(activity, QrCodeActivity.class); activity.startActivityForResult(intent, REQUEST_CODE_QR_SCAN); } - // Add this method to handle permission result + private void processImageForQRCode(Uri imageUri) { + try { + Activity currentActivity = getCurrentActivity(); + if (currentActivity == null) { + if (photoLibraryPromise != null) { + photoLibraryPromise.reject("ACTIVITY_DOES_NOT_EXIST", "Activity doesn't exist"); + photoLibraryPromise = null; + } + return; + } + + InputStream inputStream = currentActivity.getContentResolver().openInputStream(imageUri); + Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + inputStream.close(); + + if (bitmap == null) { + if (photoLibraryPromise != null) { + photoLibraryPromise.reject("IMAGE_LOAD_FAILED", "Failed to load selected image"); + photoLibraryPromise = null; + } + return; + } + + // use the exising qrcode processor we already have. + QrCodeDetectorProcessor processor = new QrCodeDetectorProcessor(); + processor.detectQrCodeInBitmap(bitmap, new QrCodeDetectorProcessor.Listener() { + @Override + public void onSuccess(String results, FrameMetadata frameMetadata, long timeRequired, Bitmap bitmap) { + if (photoLibraryPromise != null) { + photoLibraryPromise.resolve(results); + photoLibraryPromise = null; + } + } + + @Override + public void onFailure(Exception e, long timeRequired) { + if (photoLibraryPromise != null) { + photoLibraryPromise.reject("QR_DETECTION_FAILED", "No QR code found in selected image: " + e.getMessage()); + photoLibraryPromise = null; + } + } + + @Override + public void onCompletedFrame(long timeRequired) { + if (photoLibraryPromise != null) { + photoLibraryPromise.reject("QR_DETECTION_FAILED", "No QR code found in selected image"); + photoLibraryPromise = null; + } + } + }); + + } catch (Exception e) { + if (photoLibraryPromise != null) { + photoLibraryPromise.reject("IMAGE_PROCESSING_ERROR", "Error processing image: " + e.getMessage()); + photoLibraryPromise = null; + } + } + } + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (requestCode == PERMISSION_REQUEST_CAMERA) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { @@ -92,4 +229,19 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in } } } + + // Lifecycle methods + @Override + public void onHostResume() { + } + + @Override + public void onHostPause() { + } + + @Override + public void onHostDestroy() { + getReactApplicationContext().removeActivityEventListener(activityEventListener); + getReactApplicationContext().removeLifecycleEventListener(this); + } } diff --git a/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerPackage.java b/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerPackage.java index 98c9bea53..c028e0ce1 100644 --- a/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerPackage.java +++ b/app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerPackage.java @@ -21,6 +21,8 @@ public List createViewManagers(ReactApplicationContext reactContext @Override public List createNativeModules(ReactApplicationContext reactContext) { - return Collections.emptyList(); + return List.of( + new QRCodeScannerModule(reactContext) + ); } } diff --git a/app/android/app/src/main/java/com/proofofpassportapp/utils/QrCodeDetectorProcessor.kt b/app/android/app/src/main/java/com/proofofpassportapp/utils/QrCodeDetectorProcessor.kt index ea066510d..f0a5e68e3 100644 --- a/app/android/app/src/main/java/com/proofofpassportapp/utils/QrCodeDetectorProcessor.kt +++ b/app/android/app/src/main/java/com/proofofpassportapp/utils/QrCodeDetectorProcessor.kt @@ -121,27 +121,95 @@ class QrCodeDetectorProcessor { private fun detectInImage(bitmap: Bitmap): Result? { val qRCodeDetectorReader = QRCodeReader() + + // Try with original image first + var result = tryDetectInBitmap(bitmap, qRCodeDetectorReader) + if (result != null) return result + + // If original fails, try with scaled up image (better for small QR codes) + val scaledBitmap = Bitmap.createScaledBitmap(bitmap, bitmap.width * 2, bitmap.height * 2, true) + result = tryDetectInBitmap(scaledBitmap, qRCodeDetectorReader) + if (result != null) return result + + // If still fails, try with scaled down image (better for very large QR codes) + val scaledDownBitmap = Bitmap.createScaledBitmap(bitmap, bitmap.width / 2, bitmap.height / 2, true) + result = tryDetectInBitmap(scaledDownBitmap, qRCodeDetectorReader) + if (result != null) return result + + return null + } + + private fun tryDetectInBitmap(bitmap: Bitmap, qRCodeDetectorReader: QRCodeReader): Result? { + println("Attempting QR detection on bitmap: ${bitmap.width}x${bitmap.height}, hasAlpha: ${bitmap.hasAlpha()}") + val intArray = IntArray(bitmap.width * bitmap.height) bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) val source: LuminanceSource = RGBLuminanceSource(bitmap.width, bitmap.height, intArray) - val binaryBitMap = BinaryBitmap(HybridBinarizer(source)) + // Try multiple binarization strategies for better detection + val binarizers = listOf( + HybridBinarizer(source), + com.google.zxing.common.GlobalHistogramBinarizer(source) + ) + + for (binarizer in binarizers) { + val binaryBitMap = BinaryBitmap(binarizer) - try { - return qRCodeDetectorReader.decode(binaryBitMap) + try { + val result = qRCodeDetectorReader.decode(binaryBitMap) + println("QR Code detected successfully with ${binarizer.javaClass.simpleName}") + return result + } catch (e: Exception) { + println("Detection failed with ${binarizer.javaClass.simpleName}: ${e.message}") + } } - catch (e: Exception) { - // noop - println(e) + + // Try with different hints for better detection + val hints = mapOf( + com.google.zxing.DecodeHintType.TRY_HARDER to true, + com.google.zxing.DecodeHintType.POSSIBLE_FORMATS to listOf(com.google.zxing.BarcodeFormat.QR_CODE) + ) + + for (binarizer in binarizers) { + val binaryBitMap = BinaryBitmap(binarizer) + + try { + val result = qRCodeDetectorReader.decode(binaryBitMap, hints) + println("QR Code detected successfully with hints and ${binarizer.javaClass.simpleName}") + return result + } catch (e: Exception) { + println("Detection with hints failed with ${binarizer.javaClass.simpleName}: ${e.message}") + } } + + println("All QR code detection attempts failed for bitmap ${bitmap.width}x${bitmap.height}") return null } fun stop() { } + fun detectQrCodeInBitmap( + image: Bitmap, + listener: Listener + ): Boolean { + val start = System.currentTimeMillis() + executor.execute { + val result = detectInImage(image) + val timeRequired = System.currentTimeMillis() - start + println(result) + if (result != null) { + listener.onSuccess(result.text!!, null, timeRequired, null) + } + else { + listener.onCompletedFrame(timeRequired) + } + } + return true + } + interface Listener { fun onSuccess(results: String, frameMetadata: FrameMetadata?, timeRequired: Long, bitmap: Bitmap?) diff --git a/app/android/app/src/main/res/values/styles.xml b/app/android/app/src/main/res/values/styles.xml index 50f41297a..1e07c584b 100644 --- a/app/android/app/src/main/res/values/styles.xml +++ b/app/android/app/src/main/res/values/styles.xml @@ -6,4 +6,14 @@ #000000 - \ No newline at end of file + + + + diff --git a/app/ios/PhotoLibraryQRScannerViewController.swift b/app/ios/PhotoLibraryQRScannerViewController.swift new file mode 100644 index 000000000..89c26b6cc --- /dev/null +++ b/app/ios/PhotoLibraryQRScannerViewController.swift @@ -0,0 +1,152 @@ +// +// PhotoLibraryQRScannerViewController.swift +// Self +// +// Created by Rémi Colin on 09/09/2025. +// + + +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +// +// PhotoLibraryQRScannerViewController.swift +// OpenPassport +// +// Created by AI Assistant on 01/03/2025. +// + +import Foundation +import UIKit +import CoreImage +import Photos + +class PhotoLibraryQRScannerViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + var completionHandler: ((String) -> Void)? + var errorHandler: ((Error) -> Void)? + + override func viewDidLoad() { + super.viewDidLoad() + checkPhotoLibraryPermissionAndPresentPicker() + } + + private func checkPhotoLibraryPermissionAndPresentPicker() { + let status = PHPhotoLibrary.authorizationStatus() + + switch status { + case .authorized, .limited: + presentImagePicker() + case .notDetermined: + PHPhotoLibrary.requestAuthorization { [weak self] status in + DispatchQueue.main.async { + if status == .authorized || status == .limited { + self?.presentImagePicker() + } else { + self?.handlePermissionDenied() + } + } + } + case .denied, .restricted: + handlePermissionDenied() + @unknown default: + handlePermissionDenied() + } + } + + private func presentImagePicker() { + let imagePicker = UIImagePickerController() + imagePicker.delegate = self + imagePicker.sourceType = .photoLibrary + imagePicker.mediaTypes = ["public.image"] + present(imagePicker, animated: true, completion: nil) + } + + private func handlePermissionDenied() { + let error = NSError( + domain: "QRScannerError", + code: 1001, + userInfo: [NSLocalizedDescriptionKey: "Photo library access is required to scan QR codes from photos. Please enable access in Settings."] + ) + errorHandler?(error) + dismiss(animated: true, completion: nil) + } + + // MARK: - UIImagePickerControllerDelegate + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + picker.dismiss(animated: true) { [weak self] in + guard let self = self else { return } + + if let selectedImage = info[.originalImage] as? UIImage { + self.detectQRCode(in: selectedImage) + } else { + let error = NSError( + domain: "QRScannerError", + code: 1002, + userInfo: [NSLocalizedDescriptionKey: "Failed to load the selected image."] + ) + self.errorHandler?(error) + self.dismiss(animated: true, completion: nil) + } + } + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true) { [weak self] in + let error = NSError( + domain: "QRScannerError", + code: 1003, + userInfo: [NSLocalizedDescriptionKey: "User cancelled photo selection."] + ) + self?.errorHandler?(error) + self?.dismiss(animated: true, completion: nil) + } + } + + // MARK: - QR Code Detection + + private func detectQRCode(in image: UIImage) { + guard let ciImage = CIImage(image: image) else { + let error = NSError( + domain: "QRScannerError", + code: 1004, + userInfo: [NSLocalizedDescriptionKey: "Failed to process the selected image."] + ) + errorHandler?(error) + dismiss(animated: true, completion: nil) + return + } + + let detector = CIDetector( + ofType: CIDetectorTypeQRCode, + context: nil, + options: [CIDetectorAccuracy: CIDetectorAccuracyHigh] + ) + + guard let detector = detector else { + let error = NSError( + domain: "QRScannerError", + code: 1005, + userInfo: [NSLocalizedDescriptionKey: "Failed to initialize QR code detector."] + ) + errorHandler?(error) + dismiss(animated: true, completion: nil) + return + } + + let features = detector.features(in: ciImage) as? [CIQRCodeFeature] ?? [] + + if let firstQRCode = features.first, let qrCodeString = firstQRCode.messageString { + completionHandler?(qrCodeString) + dismiss(animated: true, completion: nil) + } else { + let error = NSError( + domain: "QRScannerError", + code: 1006, + userInfo: [NSLocalizedDescriptionKey: "No QR code found in the selected image. Please try with a different image."] + ) + errorHandler?(error) + dismiss(animated: true, completion: nil) + } + } +} + diff --git a/app/ios/QRScannerBridge.m b/app/ios/QRScannerBridge.m index 9410cd2c7..34c943cab 100644 --- a/app/ios/QRScannerBridge.m +++ b/app/ios/QRScannerBridge.m @@ -14,4 +14,6 @@ @interface RCT_EXTERN_MODULE(QRScannerBridge, NSObject) RCT_EXTERN_METHOD(scanQRCode:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(scanQRCodeFromPhotoLibrary:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) + @end diff --git a/app/ios/QRScannerBridge.swift b/app/ios/QRScannerBridge.swift index 4bcf0ad63..1508fe144 100644 --- a/app/ios/QRScannerBridge.swift +++ b/app/ios/QRScannerBridge.swift @@ -10,6 +10,8 @@ import Foundation import SwiftQRScanner import React +import UIKit +import CoreImage @objc(QRScannerBridge) class QRScannerBridge: NSObject { @@ -29,4 +31,19 @@ class QRScannerBridge: NSObject { rootViewController?.present(qrScannerViewController, animated: true, completion: nil) } } + + @objc + func scanQRCodeFromPhotoLibrary(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { + DispatchQueue.main.async { + let rootViewController = UIApplication.shared.keyWindow?.rootViewController + let photoLibraryQRScanner = PhotoLibraryQRScannerViewController() + photoLibraryQRScanner.completionHandler = { result in + resolve(result) + } + photoLibraryQRScanner.errorHandler = { error in + reject("QR_SCAN_ERROR", error.localizedDescription, error) + } + rootViewController?.present(photoLibraryQRScanner, animated: true, completion: nil) + } + } } diff --git a/app/ios/Self.xcodeproj/project.pbxproj b/app/ios/Self.xcodeproj/project.pbxproj index d894e12ef..78a4b0457 100644 --- a/app/ios/Self.xcodeproj/project.pbxproj +++ b/app/ios/Self.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 165E76BD2B8DC4A00000FA90 /* MRZScannerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165E76BC2B8DC4A00000FA90 /* MRZScannerModule.swift */; }; 165E76BF2B8DC53A0000FA90 /* MRZScannerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 165E76BE2B8DC53A0000FA90 /* MRZScannerModule.m */; }; 165E76C32B8DC8370000FA90 /* ScannerHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165E76C22B8DC8370000FA90 /* ScannerHostingController.swift */; }; + 1668A53F2E70A55E0005A522 /* PhotoLibraryQRScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1668A53E2E70A55E0005A522 /* PhotoLibraryQRScannerViewController.swift */; }; 1686F0DC2C500F3800841CDE /* QRScannerBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1686F0DB2C500F3800841CDE /* QRScannerBridge.swift */; }; 1686F0DE2C500F4F00841CDE /* QRScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1686F0DD2C500F4F00841CDE /* QRScannerViewController.swift */; }; 1686F0E02C500FBD00841CDE /* QRScannerBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 1686F0DF2C500FBD00841CDE /* QRScannerBridge.m */; }; @@ -57,6 +58,7 @@ 165E76BC2B8DC4A00000FA90 /* MRZScannerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MRZScannerModule.swift; sourceTree = ""; }; 165E76BE2B8DC53A0000FA90 /* MRZScannerModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MRZScannerModule.m; sourceTree = ""; }; 165E76C22B8DC8370000FA90 /* ScannerHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScannerHostingController.swift; sourceTree = ""; }; + 1668A53E2E70A55E0005A522 /* PhotoLibraryQRScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryQRScannerViewController.swift; sourceTree = ""; }; 1686F0DB2C500F3800841CDE /* QRScannerBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerBridge.swift; sourceTree = ""; }; 1686F0DD2C500F4F00841CDE /* QRScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerViewController.swift; sourceTree = ""; }; 1686F0DF2C500FBD00841CDE /* QRScannerBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QRScannerBridge.m; sourceTree = ""; }; @@ -102,6 +104,7 @@ 13B07FAE1A68108700A75B9A /* OpenPassport */ = { isa = PBXGroup; children = ( + 1668A53E2E70A55E0005A522 /* PhotoLibraryQRScannerViewController.swift */, BF6F0D542E38ED81008EA85C /* SelfAnalytics.swift */, BFBA0C782E33A01F00E82A52 /* NativeLoggerBridge.m */, BFBA0C762E339D2B00E82A52 /* NativeLoggerBridge.swift */, @@ -405,6 +408,7 @@ 1648EB782CC9564D003BEA7D /* LottieView.swift in Sources */, 164FD9672D569A640067E63B /* QRCodeScannerViewManager.swift in Sources */, 165E76BD2B8DC4A00000FA90 /* MRZScannerModule.swift in Sources */, + 1668A53F2E70A55E0005A522 /* PhotoLibraryQRScannerViewController.swift in Sources */, BF6F0D552E38ED81008EA85C /* SelfAnalytics.swift in Sources */, BF1044812DD53540009B3688 /* LiveMRZScannerView.swift in Sources */, 164FD9692D569C1F0067E63B /* QRCodeScannerViewManager.m in Sources */, @@ -785,10 +789,7 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -878,10 +879,7 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/app/src/components/NavBar/AadhaarNavBar.tsx b/app/src/components/NavBar/AadhaarNavBar.tsx new file mode 100644 index 000000000..39ea21f60 --- /dev/null +++ b/app/src/components/NavBar/AadhaarNavBar.tsx @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Button, XStack, YStack } from 'tamagui'; +import type { NativeStackHeaderProps } from '@react-navigation/native-stack'; +import { ChevronLeft, HelpCircle } from '@tamagui/lucide-icons'; + +import { NavBar } from '@/components/NavBar/BaseNavBar'; +import { black, slate100, slate300 } from '@/utils/colors'; +import { extraYPadding } from '@/utils/constants'; +import { dinot } from '@/utils/fonts'; +import { buttonTap } from '@/utils/haptic'; + +export const AadhaarNavBar = (props: NativeStackHeaderProps) => { + const insets = useSafeAreaInsets(); + + const currentRouteName = props.route.name; + const isFirstStep = currentRouteName === 'AadhaarUpload'; + const isSecondStep = + currentRouteName === 'AadhaarUploadSuccess' || + currentRouteName === 'AadhaarUploadError'; + + const handleClose = () => { + buttonTap(); + props.navigation.goBack(); + }; + + const handleHelp = () => { + buttonTap(); + // Handle help action - could open a modal or navigate to help screen + console.log('Help pressed'); + }; + + return ( + + + + + + } + /> + + + AADHAAR REGISTRATION + + + + + + } + /> + + + {/* Progress Bar - dynamic based on current step */} + + + + + + + + ); +}; diff --git a/app/src/components/homeScreen/idCard.tsx b/app/src/components/homeScreen/idCard.tsx index 64fa39f36..a997124e1 100644 --- a/app/src/components/homeScreen/idCard.tsx +++ b/app/src/components/homeScreen/idCard.tsx @@ -6,13 +6,19 @@ import type { FC } from 'react'; import { Dimensions } from 'react-native'; import { Separator, Text, XStack, YStack } from 'tamagui'; +import { + AadhaarData, + isAadhaarDocument, + isMRZDocument, + PassportData, +} from '@selfxyz/common'; import { attributeToPosition, attributeToPosition_ID, } from '@selfxyz/common/constants'; -import { PassportData } from '@selfxyz/common/types'; import { SvgXml } from '@/components/homeScreen/SvgXmlWrapper'; +import AadhaarIcon from '@/images/icons/aadhaar.svg'; import EPassport from '@/images/icons/epassport.svg'; import LogoGray from '@/images/logo_gray.svg'; import { @@ -33,7 +39,7 @@ const logoSvg = ``; interface IdCardLayoutAttributes { - idDocument: PassportData; + idDocument: PassportData | AadhaarData | null; selected: boolean; hidden: boolean; } @@ -49,6 +55,11 @@ const IdCardLayout: FC = ({ selected, hidden, }) => { + // Early return if document is null + if (!idDocument) { + return null; + } + // Function to mask MRZ characters except '<' and spaces const maskMrzValue = (text: string): string => { return text.replace(/./g, 'X'); @@ -107,10 +118,17 @@ const IdCardLayout: FC = ({ {/* Header Section */} - + {idDocument.documentCategory === 'aadhaar' ? ( + + ) : ( + + )} = ({ > {idDocument.documentCategory === 'passport' ? 'Passport' - : 'ID Card'} + : idDocument.documentCategory === 'aadhaar' + ? 'Aadhaar' + : 'ID Card'} = ({ Verified{' '} {idDocument.documentCategory === 'passport' ? 'Biometric Passport' - : ' Biometric ID Card'} + : idDocument.documentCategory === 'aadhaar' + ? 'Aadhaar Document' + : 'Biometric ID Card'} @@ -203,12 +225,16 @@ const IdCardLayout: FC = ({ value={ idDocument.documentCategory === 'passport' ? 'PASSPORT' - : 'ID CARD' + : idDocument.documentCategory === 'aadhaar' + ? 'AADHAAR' + : 'ID CARD' } maskValue={ idDocument.documentCategory === 'passport' ? 'PASSPORT' - : 'ID CARD' + : idDocument.documentCategory === 'aadhaar' + ? 'AADHAAR' + : 'ID CARD' } hidden={hidden} /> @@ -224,68 +250,81 @@ const IdCardLayout: FC = ({ - - - - - - - - - + {idDocument.documentCategory === 'aadhaar' ? ( + // Aadhaar: Combined name field spanning two columns + <> + + { + const nameData = getNameAndSurname( + getDocumentAttributes(idDocument).nameSlice, + ); + const fullName = [ + ...nameData.surname, + ...nameData.names, + ].join(' '); + return fullName; + })()} + maskValue="XXXXXXXXXXXXX" + hidden={hidden} + /> + + + + + + ) : ( + // Other documents: Separate surname and name fields + <> + + + + + + + + + + + )} @@ -294,10 +333,7 @@ const IdCardLayout: FC = ({ )} - {/* Footer Section - MRZ */} - {selected && ( + {/* Footer Section - MRZ or QR Data */} + {selected && isMRZDocument(idDocument) && idDocument.mrz && ( = ({ )} + + {/* Footer Section - Empty placeholder for Aadhaar (no MRZ) */} + {selected && isAadhaarDocument(idDocument) && ( + + {/* Fixed-width spacer to align content with the attributes block */} + + + + + + + {/* Empty placeholder - no MRZ for Aadhaar */} + + + + )} ); @@ -466,6 +523,65 @@ const IdAttribute: FC = ({ export default IdCardLayout; +// Helper functions to safely extract document data +function getDocumentAttributes(document: PassportData | AadhaarData) { + if (isAadhaarDocument(document)) { + return getAadhaarAttributes(document); + } else if (isMRZDocument(document)) { + return getPassportAttributes(document.mrz, document.documentCategory); + } else { + // Fallback for unknown document types + return { + nameSlice: '', + dobSlice: '', + yobSlice: '', + issuingStateSlice: '', + nationalitySlice: '', + passNoSlice: '', + sexSlice: '', + expiryDateSlice: '', + isPassportType: false, + }; + } +} + +function getAadhaarAttributes(document: AadhaarData) { + const extractedFields = document.extractedFields; + // For Aadhaar, we format the name to work with the existing getNameAndSurname function + // We'll put the full name in the "surname" position and leave names empty + const fullName = extractedFields?.name || ''; + const nameSliceFormatted = fullName ? `${fullName}<<` : ''; // Format like MRZ + + // Format DOB to YYMMDD for consistency with passport format + let dobFormatted = ''; + if (extractedFields?.dob && extractedFields?.mob && extractedFields?.yob) { + const year = + extractedFields.yob.length === 4 + ? extractedFields.yob.slice(-2) + : extractedFields.yob; + const month = extractedFields.mob.padStart(2, '0'); + const day = extractedFields.dob.padStart(2, '0'); + dobFormatted = `${year}${month}${day}`; + } + + return { + nameSlice: nameSliceFormatted, + dobSlice: dobFormatted, + yobSlice: extractedFields?.yob || '', + issuingStateSlice: extractedFields?.state || '', + nationalitySlice: 'IND', // Aadhaar is always Indian + passNoSlice: extractedFields?.aadhaarLast4Digits || '', + sexSlice: + extractedFields?.gender === 'M' + ? 'M' + : extractedFields?.gender === 'F' + ? 'F' + : extractedFields?.gender || '', + expiryDateSlice: '', // Aadhaar doesn't expire + isPassportType: false, + }; +} + function getPassportAttributes(mrz: string, documentCategory: string) { const isPassportType = documentCategory === 'passport'; const attributePositions = isPassportType diff --git a/app/src/hooks/useMockDataForm.ts b/app/src/hooks/useMockDataForm.ts index 6c85be7a4..12cd9c5f1 100644 --- a/app/src/hooks/useMockDataForm.ts +++ b/app/src/hooks/useMockDataForm.ts @@ -12,7 +12,7 @@ export const useMockDataForm = () => { 'sha256 rsa 65537 2048', ); const [selectedDocumentType, setSelectedDocumentType] = useState< - 'mock_passport' | 'mock_id_card' + 'mock_passport' | 'mock_id_card' | 'mock_aadhaar' >('mock_passport'); const [isInOfacList, setIsInOfacList] = useState(true); @@ -34,7 +34,7 @@ export const useMockDataForm = () => { }; const handleDocumentTypeSelect = ( - documentType: 'mock_passport' | 'mock_id_card', + documentType: 'mock_passport' | 'mock_id_card' | 'mock_aadhaar', ) => { setSelectedDocumentType(documentType); }; diff --git a/app/src/images/512w.png b/app/src/images/512w.png new file mode 100644 index 000000000..b20d9981b Binary files /dev/null and b/app/src/images/512w.png differ diff --git a/app/src/images/blue_check.svg b/app/src/images/blue_check.svg new file mode 100644 index 000000000..19f94605a --- /dev/null +++ b/app/src/images/blue_check.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/images/icons/aadhaar.svg b/app/src/images/icons/aadhaar.svg new file mode 100644 index 000000000..686c965c7 --- /dev/null +++ b/app/src/images/icons/aadhaar.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/images/warning.svg b/app/src/images/warning.svg new file mode 100644 index 000000000..5e8bce59f --- /dev/null +++ b/app/src/images/warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/navigation/home.ts b/app/src/navigation/home.ts index b05195739..dcedddebd 100644 --- a/app/src/navigation/home.ts +++ b/app/src/navigation/home.ts @@ -5,6 +5,10 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stack'; import { HomeNavBar, IdDetailsNavBar } from '@/components/NavBar'; +import { AadhaarNavBar } from '@/components/NavBar/AadhaarNavBar'; +import AadhaarUploadedSuccessScreen from '@/screens/document/aadhaar/AadhaarUploadedSuccessScreen'; +import AadhaarUploadErrorScreen from '@/screens/document/aadhaar/AadhaarUploadErrorScreen'; +import AadhaarUploadScreen from '@/screens/document/aadhaar/AadhaarUploadScreen'; import DisclaimerScreen from '@/screens/home/DisclaimerScreen'; import HomeScreen from '@/screens/home/HomeScreen'; import IdDetailsScreen from '@/screens/home/IdDetailsScreen'; @@ -48,6 +52,33 @@ const homeScreens = { headerBackVisible: false, // Hide default back button }, }, + AadhaarUpload: { + screen: AadhaarUploadScreen, + options: { + title: 'AADHAAR REGISTRATION', + header: AadhaarNavBar, + headerBackVisible: false, + } as NativeStackNavigationOptions, + }, + AadhaarUploadSuccess: { + screen: AadhaarUploadedSuccessScreen, + options: { + title: 'AADHAAR REGISTRATION', + header: AadhaarNavBar, + headerBackVisible: false, + } as NativeStackNavigationOptions, + }, + AadhaarUploadError: { + screen: AadhaarUploadErrorScreen, + options: { + title: 'AADHAAR REGISTRATION', + header: AadhaarNavBar, + headerBackVisible: false, + } as NativeStackNavigationOptions, + initialParams: { + errorType: 'general', + }, + }, }; export default homeScreens; diff --git a/app/src/providers/passportDataProvider.tsx b/app/src/providers/passportDataProvider.tsx index 6c0effd2c..a6c14ed9c 100644 --- a/app/src/providers/passportDataProvider.tsx +++ b/app/src/providers/passportDataProvider.tsx @@ -44,6 +44,7 @@ import type { PropsWithChildren } from 'react'; import React, { createContext, useCallback, useContext, useMemo } from 'react'; import Keychain from 'react-native-keychain'; +import { isMRZDocument } from '@selfxyz/common'; import type { PublicKeyDetailsECDSA, PublicKeyDetailsRSA, @@ -55,8 +56,10 @@ import { parseCertificateSimple, } from '@selfxyz/common/utils'; import type { + AadhaarData, DocumentCatalog, DocumentMetadata, + IDDocument, PassportData, } from '@selfxyz/common/utils/types'; import type { DocumentsAdapter, SelfClient } from '@selfxyz/mobile-sdk-alpha'; @@ -387,10 +390,10 @@ export async function initializeNativeModules( // TODO: is this used? async function loadAllPassportData(selfClient: SelfClient): Promise<{ - [service: string]: PassportData; + [service: string]: IDDocument; }> { const allDocs = await getAllDocuments(selfClient); - const result: { [service: string]: PassportData } = {}; + const result: { [service: string]: IDDocument } = {}; // Convert to legacy format for backward compatibility Object.values(allDocs).forEach(({ data, metadata }) => { @@ -601,7 +604,7 @@ interface IPassportContext { data: PassportData; } | null>; // TODO: is this even used? - getAllData: () => Promise<{ [service: string]: PassportData }>; + getAllData: () => Promise<{ [service: string]: IDDocument }>; getAvailableTypes: () => Promise; setData: (data: PassportData) => Promise; getPassportDataAndSecret: () => Promise<{ @@ -616,7 +619,7 @@ interface IPassportContext { loadDocumentCatalog: () => Promise; getAllDocuments: () => Promise<{ - [documentId: string]: { data: PassportData; metadata: DocumentMetadata }; + [documentId: string]: { data: IDDocument; metadata: DocumentMetadata }; }>; setSelectedDocument: (documentId: string) => Promise; @@ -742,7 +745,7 @@ export async function setSelectedDocument(documentId: string): Promise { async function storeDocumentDirectlyToKeychain( contentHash: string, - passportData: PassportData, + passportData: PassportData | AadhaarData, ): Promise { await Keychain.setGenericPassword(contentHash, JSON.stringify(passportData), { service: `document-${contentHash}`, @@ -750,7 +753,7 @@ async function storeDocumentDirectlyToKeychain( } export async function storeDocumentWithDeduplication( - passportData: PassportData, + passportData: PassportData | AadhaarData, ): Promise { const contentHash = calculateContentHash(passportData); const catalog = await loadDocumentCatalogDirectlyFromKeychain(); @@ -780,8 +783,12 @@ export async function storeDocumentWithDeduplication( documentType: passportData.documentType, documentCategory: passportData.documentCategory || - inferDocumentCategory(passportData.documentType), - data: passportData.mrz || '', // Store MRZ for passports/IDs, relevant data for aadhaar + inferDocumentCategory( + (passportData as PassportData | AadhaarData).documentType, + ), + data: isMRZDocument(passportData) + ? (passportData as PassportData).mrz + : (passportData as AadhaarData).qrData || '', // Store MRZ for passports/IDs, relevant data for aadhaar mock: passportData.mock || false, isRegistered: false, }; @@ -793,7 +800,9 @@ export async function storeDocumentWithDeduplication( return contentHash; } -export async function storePassportData(passportData: PassportData) { +export async function storePassportData( + passportData: PassportData | AadhaarData, +) { await storeDocumentWithDeduplication(passportData); } diff --git a/app/src/screens/dev/CreateMockScreen.tsx b/app/src/screens/dev/CreateMockScreen.tsx index 7f766c1ab..f29f98b59 100644 --- a/app/src/screens/dev/CreateMockScreen.tsx +++ b/app/src/screens/dev/CreateMockScreen.tsx @@ -57,6 +57,7 @@ import { buttonTap, selectionChange } from '@/utils/haptic'; const documentTypes = { mock_passport: 'Passport', mock_id_card: 'ID Card', + mock_aadhaar: 'Aadhaar', }; const MockDocumentTitleCard = () => { @@ -181,6 +182,10 @@ const CreateMockScreen: React.FC = () => { const handleGenerate = useCallback(async () => { setIsGenerating(true); + + // Allow React to update the UI state + await new Promise(resolve => setTimeout(resolve, 0)); + try { const parsedMockData = await generateMockDocument({ age, @@ -237,27 +242,29 @@ const CreateMockScreen: React.FC = () => { borderColor={slate200} backgroundColor={slate100} > - - - + {selectedDocumentType !== 'mock_aadhaar' && ( + + + + )} - - - - - - + {selectedDocumentType !== 'mock_aadhaar' && ( + - - {age} years or older - - - - + + )} - + { borderWidth={1} onPress={() => { buttonTap(); - setExpiryYears(expiryYears - 1); - trackEvent(MockDataEvents.DECREASE_EXPIRY_YEARS); + setAge(age - 1); + trackEvent(MockDataEvents.DECREASE_AGE); }} - disabled={expiryYears <= 0} + disabled={age <= 1} > @@ -400,7 +363,7 @@ const CreateMockScreen: React.FC = () => { fontSize="$4" fontFamily={plexMono} > - {expiryYears} years + {age} years or older + {selectedDocumentType !== 'mock_aadhaar' && ( + + + + + {expiryYears} years + + + + + )} + { onPress={() => { buttonTap(); handleDocumentTypeSelect( - docType as 'mock_passport' | 'mock_id_card', + docType as + | 'mock_passport' + | 'mock_id_card' + | 'mock_aadhaar', ); setDocumentTypeSheetOpen(false); trackEvent(MockDataEvents.SELECT_DOCUMENT_TYPE); diff --git a/app/src/screens/dev/DevSettingsScreen.tsx b/app/src/screens/dev/DevSettingsScreen.tsx index a6a559ed4..d5533dcbf 100644 --- a/app/src/screens/dev/DevSettingsScreen.tsx +++ b/app/src/screens/dev/DevSettingsScreen.tsx @@ -124,6 +124,7 @@ function ParameterSection({ const items = [ 'DevSettings', + 'AadhaarUpload', 'DevFeatureFlags', 'DevHapticFeedback', 'DevPrivateKey', diff --git a/app/src/screens/document/aadhaar/AadhaarUploadErrorScreen.tsx b/app/src/screens/document/aadhaar/AadhaarUploadErrorScreen.tsx new file mode 100644 index 000000000..73f103459 --- /dev/null +++ b/app/src/screens/document/aadhaar/AadhaarUploadErrorScreen.tsx @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { XStack, YStack } from 'tamagui'; +import type { RouteProp } from '@react-navigation/native'; +import { useNavigation, useRoute } from '@react-navigation/native'; + +import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; +import { AadhaarEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; + +import { PrimaryButton } from '@/components/buttons/PrimaryButton'; +import { SecondaryButton } from '@/components/buttons/SecondaryButton'; +import { BodyText } from '@/components/typography/BodyText'; +import WarningIcon from '@/images/warning.svg'; +import { useSafeAreaInsets } from '@/mocks/react-native-safe-area-context'; +import { black, slate100, slate200, slate500, white } from '@/utils/colors'; +import { extraYPadding } from '@/utils/constants'; + +type AadhaarUploadErrorRouteParams = { + errorType?: 'general' | 'expired'; +}; + +type AadhaarUploadErrorRoute = RouteProp< + Record, + string +>; + +const AadhaarUploadErrorScreen: React.FC = () => { + const { bottom } = useSafeAreaInsets(); + const navigation = useNavigation(); + const route = useRoute(); + const { trackEvent } = useSelfClient(); + const errorType = route.params?.errorType || 'general'; + + // Define error messages based on error type + const getErrorMessages = () => { + if (errorType === 'expired') { + return { + title: 'QR Code Has Expired', + description: + 'You uploaded a valid Aadhaar QR code, but unfortunately it has expired. Please generate a new QR code from the mAadhaar app and try again.', + }; + } + + return { + title: 'There was a problem reading the code', + description: + 'Please ensure the QR code is clear and well-lit, then try again. For best results, take a screenshot of the QR code instead of photographing it.', + }; + }; + + const { title, description } = getErrorMessages(); + + return ( + + + + + + + + + + {title} + + + {description} + + + + + + + { + trackEvent(AadhaarEvents.RETRY_BUTTON_PRESSED, { errorType }); + // Navigate back to upload screen to try again + navigation.goBack(); + }} + > + Try Again + + + + { + trackEvent(AadhaarEvents.HELP_BUTTON_PRESSED, { errorType }); + // TODO: Implement help functionality + }} + > + Need Help? + + + + + + ); +}; + +export default AadhaarUploadErrorScreen; diff --git a/app/src/screens/document/aadhaar/AadhaarUploadScreen.tsx b/app/src/screens/document/aadhaar/AadhaarUploadScreen.tsx new file mode 100644 index 000000000..58acf4eef --- /dev/null +++ b/app/src/screens/document/aadhaar/AadhaarUploadScreen.tsx @@ -0,0 +1,330 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React, { useCallback, useEffect, useState } from 'react'; +import { Linking } from 'react-native'; +import { Image, XStack, YStack } from 'tamagui'; +import { useNavigation } from '@react-navigation/native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +import { + extractQRDataFields, + getAadharRegistrationWindow, +} from '@selfxyz/common/utils'; +import type { AadhaarData } from '@selfxyz/common/utils/types'; +import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; +import { AadhaarEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; + +import { PrimaryButton } from '@/components/buttons/PrimaryButton'; +import { BodyText } from '@/components/typography/BodyText'; +import { useModal } from '@/hooks/useModal'; +import AadhaarImage from '@/images/512w.png'; +import { useSafeAreaInsets } from '@/mocks/react-native-safe-area-context'; +import type { RootStackParamList } from '@/navigation'; +import { storePassportData } from '@/providers/passportDataProvider'; +import { slate100, slate200, slate400, slate500, white } from '@/utils/colors'; +import { extraYPadding } from '@/utils/constants'; +import { + isQRScannerPhotoLibraryAvailable, + scanQRCodeFromPhotoLibrary, +} from '@/utils/qrScanner'; + +const AadhaarUploadScreen: React.FC = () => { + const { bottom } = useSafeAreaInsets(); + const navigation = + useNavigation>(); + const { trackEvent } = useSelfClient(); + const [isProcessing, setIsProcessing] = useState(false); + + const { showModal: showPermissionModal } = useModal({ + titleText: 'Photo Library Access Required', + bodyText: + 'To upload QR codes from your photo library, please enable photo library access in your device settings.', + buttonText: 'Open Settings', + secondaryButtonText: 'Cancel', + onButtonPress: () => { + trackEvent(AadhaarEvents.PERMISSION_SETTINGS_OPENED); + Linking.openSettings(); + }, + onModalDismiss: () => { + trackEvent(AadhaarEvents.PERMISSION_MODAL_DISMISSED); + }, + }); + + // Track screen entry + useEffect(() => { + trackEvent(AadhaarEvents.UPLOAD_SCREEN_OPENED); + + // Track button state based on photo library availability + if (isQRScannerPhotoLibraryAvailable()) { + trackEvent(AadhaarEvents.UPLOAD_BUTTON_ENABLED); + } else { + trackEvent(AadhaarEvents.UPLOAD_BUTTON_DISABLED); + trackEvent(AadhaarEvents.PHOTO_LIBRARY_UNAVAILABLE); + } + }, [trackEvent]); + + const validateAAdhaarTimestamp = useCallback( + async (timestamp: string) => { + //timestamp is in YYYY-MM-DD HH:MM format + trackEvent(AadhaarEvents.TIMESTAMP_VALIDATION_STARTED); + + const currentTimestamp = new Date().getTime(); + const timestampDate = new Date(timestamp); + const timestampTimestamp = timestampDate.getTime(); + const diff = currentTimestamp - timestampTimestamp; + const diffMinutes = diff / (1000 * 60); + + const allowedWindow = await getAadharRegistrationWindow(); + const isValid = diffMinutes <= allowedWindow; + + if (isValid) { + trackEvent(AadhaarEvents.TIMESTAMP_VALIDATION_SUCCESS); + } else { + trackEvent(AadhaarEvents.TIMESTAMP_VALIDATION_FAILED); + } + + return isValid; + }, + [trackEvent], + ); + + const processAadhaarQRCode = useCallback( + async (qrCodeData: string) => { + try { + if ( + !qrCodeData || + typeof qrCodeData !== 'string' || + qrCodeData.length < 100 + ) { + trackEvent(AadhaarEvents.QR_CODE_INVALID_FORMAT); + throw new Error('Invalid QR code format - too short or not a string'); + } + + if (!/^\d+$/.test(qrCodeData)) { + trackEvent(AadhaarEvents.QR_CODE_INVALID_FORMAT); + throw new Error('Invalid QR code format - not a numeric string'); + } + + if (qrCodeData.length < 100) { + trackEvent(AadhaarEvents.QR_CODE_INVALID_FORMAT); + throw new Error( + 'QR code too short - likely not a valid Aadhaar QR code', + ); + } + + trackEvent(AadhaarEvents.QR_DATA_EXTRACTION_STARTED); + let extractedFields; + try { + extractedFields = extractQRDataFields(qrCodeData); + trackEvent(AadhaarEvents.QR_DATA_EXTRACTION_SUCCESS); + } catch { + trackEvent(AadhaarEvents.QR_CODE_PARSE_FAILED); + throw new Error('Failed to parse Aadhaar QR code - invalid format'); + } + + if ( + !extractedFields.name || + !extractedFields.dob || + !extractedFields.gender + ) { + trackEvent(AadhaarEvents.QR_CODE_MISSING_FIELDS); + throw new Error('Invalid Aadhaar QR code - missing required fields'); + } + + if (!(await validateAAdhaarTimestamp(extractedFields.timestamp))) { + trackEvent(AadhaarEvents.QR_CODE_EXPIRED); + throw new Error('QRCODE_EXPIRED'); + } + + const aadhaarData: AadhaarData = { + documentType: 'aadhaar', + documentCategory: 'aadhaar', + mock: false, + qrData: qrCodeData, + extractedFields: extractedFields, + signature: [], + publicKey: '', + photoHash: '', + }; + + trackEvent(AadhaarEvents.DATA_STORAGE_STARTED); + await storePassportData(aadhaarData); + trackEvent(AadhaarEvents.DATA_STORAGE_SUCCESS); + + trackEvent(AadhaarEvents.QR_UPLOAD_SUCCESS); + + navigation.navigate('AadhaarUploadSuccess'); + } catch (error) { + // Check if it's a QR code expiration error + const errorType: 'expired' | 'general' = + error instanceof Error && error.message === 'QRCODE_EXPIRED' + ? 'expired' + : 'general'; + + trackEvent(AadhaarEvents.ERROR_SCREEN_NAVIGATED, { errorType }); + (navigation.navigate as any)('AadhaarUploadError', { errorType }); + } + }, + [navigation, trackEvent, validateAAdhaarTimestamp], + ); + + const onPhotoLibraryPress = useCallback(async () => { + if (isProcessing) { + return; + } + + try { + setIsProcessing(true); + trackEvent(AadhaarEvents.PROCESSING_STARTED); + + const qrCodeData = await scanQRCodeFromPhotoLibrary(); + await processAadhaarQRCode(qrCodeData); + } catch (error) { + trackEvent(AadhaarEvents.QR_UPLOAD_FAILED, { + error: + error instanceof Error + ? error.message + : error?.toString() || 'Unknown error', + }); + + // Don't show error for user cancellation + if (error instanceof Error && error.message.includes('cancelled')) { + trackEvent(AadhaarEvents.USER_CANCELLED_SELECTION); + return; + } + + // Handle permission errors specifically - check for exact message from native code + const errorMessage = + error instanceof Error ? error.message : String(error); + + if (errorMessage.includes('Photo library access is required')) { + trackEvent(AadhaarEvents.PERMISSION_MODAL_OPENED); + showPermissionModal(); + return; + } + + // Also check for other permission-related error messages + if ( + errorMessage.includes('permission') || + errorMessage.includes('access') || + errorMessage.includes('Settings') || + errorMessage.includes('enable access') + ) { + trackEvent(AadhaarEvents.PERMISSION_MODAL_OPENED); + showPermissionModal(); + return; + } + + // Handle QR code scanning/processing errors + if ( + errorMessage.includes('No QR code found') || + errorMessage.includes('QR code') || + errorMessage.includes('Failed to process') || + errorMessage.includes('Invalid') + ) { + (navigation.navigate as any)('AadhaarUploadError', { + errorType: 'general' as const, + }); + return; + } + + // Handle any other errors by showing error screen + (navigation.navigate as any)('AadhaarUploadError', { + errorType: 'general' as const, + }); + } finally { + setIsProcessing(false); + } + }, [ + isProcessing, + trackEvent, + processAadhaarQRCode, + navigation, + showPermissionModal, + ]); + + return ( + + + + + + + + + + Generate a QR code from the mAadaar app + + + Save the QR code to your photo library and upload it here. + + + SELF DOES NOT STORE THIS INFORMATION. + + + + + + + + {isProcessing ? 'Processing...' : 'Upload QR code'} + + + {/* TODO: Implement camera-based QR scanning for Aadhaar */} + {/* */} + + + + ); +}; + +export default AadhaarUploadScreen; diff --git a/app/src/screens/document/aadhaar/AadhaarUploadedSuccessScreen.tsx b/app/src/screens/document/aadhaar/AadhaarUploadedSuccessScreen.tsx new file mode 100644 index 000000000..c9df4bf8f --- /dev/null +++ b/app/src/screens/document/aadhaar/AadhaarUploadedSuccessScreen.tsx @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { YStack } from 'tamagui'; +import { useNavigation } from '@react-navigation/native'; + +import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; +import { AadhaarEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; + +import { PrimaryButton } from '@/components/buttons/PrimaryButton'; +import { BodyText } from '@/components/typography/BodyText'; +import BlueCheckIcon from '@/images/blue_check.svg'; +import { useSafeAreaInsets } from '@/mocks/react-native-safe-area-context'; +import { black, slate100, slate200, slate500, white } from '@/utils/colors'; +import { extraYPadding } from '@/utils/constants'; + +const AadhaarUploadedSuccessScreen: React.FC = () => { + const { bottom } = useSafeAreaInsets(); + const navigation = useNavigation(); + const { trackEvent } = useSelfClient(); + + return ( + + + + + + + + + + QR code upload successful + + + You are ready to register your Aadhaar card with Self. + + + + + { + trackEvent(AadhaarEvents.CONTINUE_TO_REGISTRATION_PRESSED); + navigation.navigate('ConfirmBelonging', {}); + }} + > + Continue to Registration + + + + ); +}; + +export default AadhaarUploadedSuccessScreen; diff --git a/app/src/screens/home/HomeScreen.tsx b/app/src/screens/home/HomeScreen.tsx index cbde3b92c..bbb765f24 100644 --- a/app/src/screens/home/HomeScreen.tsx +++ b/app/src/screens/home/HomeScreen.tsx @@ -2,10 +2,10 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { Pressable } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { Button, ScrollView, styled, Text, YStack } from 'tamagui'; +import { ScrollView, Text, YStack } from 'tamagui'; import { useFocusEffect, useNavigation, @@ -13,48 +13,31 @@ import { } from '@react-navigation/native'; import { PassportData } from '@selfxyz/common/types'; -import { DocumentCatalog } from '@selfxyz/common/utils/types'; +import { DocumentCatalog, IDDocument } from '@selfxyz/common/utils/types'; import { DocumentMetadata, useSelfClient } from '@selfxyz/mobile-sdk-alpha'; -import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; +import { DocumentEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; -import { pressedStyle } from '@/components/buttons/pressedStyle'; import IdCardLayout from '@/components/homeScreen/idCard'; -import { BodyText } from '@/components/typography/BodyText'; import { useAppUpdates } from '@/hooks/useAppUpdates'; import useConnectionModal from '@/hooks/useConnectionModal'; -import useHapticNavigation from '@/hooks/useHapticNavigation'; -import WarnIcon from '@/images/icons/warning.svg'; import { usePassport } from '@/providers/passportDataProvider'; -import { useSettingStore } from '@/stores/settingStore'; import useUserStore from '@/stores/userStore'; -import { neutral700, slate50, slate800, white } from '@/utils/colors'; +import { slate50 } from '@/utils/colors'; import { extraYPadding } from '@/utils/constants'; -const ScanButton = styled(Button, { - borderRadius: 20, - width: 90, - height: 90, - borderColor: neutral700, - borderWidth: 1, - backgroundColor: '#1D1D1D', - alignItems: 'center', - justifyContent: 'center', -}); - const HomeScreen: React.FC = () => { const selfClient = useSelfClient(); useConnectionModal(); const navigation = useNavigation(); const { setIdDetailsDocumentId } = useUserStore(); - const { getAllDocuments, loadDocumentCatalog, setSelectedDocument } = - usePassport(); + const { getAllDocuments, loadDocumentCatalog } = usePassport(); const [isNewVersionAvailable, showAppUpdateModal, isModalDismissed] = useAppUpdates(); const [documentCatalog, setDocumentCatalog] = useState({ documents: [], }); const [allDocuments, setAllDocuments] = useState< - Record + Record >({}); const [loading, setLoading] = useState(true); @@ -89,22 +72,6 @@ const HomeScreen: React.FC = () => { } }); - const handleDocumentSelection = async (documentId: string) => { - await setSelectedDocument(documentId); - // Reload catalog to update selected state - const updatedCatalog = await loadDocumentCatalog(); - setDocumentCatalog(updatedCatalog); - }; - - const goToQRCodeViewFinder = useHapticNavigation('QRCodeViewFinder'); - const onScanButtonPress = useCallback(() => { - selfClient.trackEvent(ProofEvents.QR_SCAN_REQUESTED, { - from: 'Home', - }); - - goToQRCodeViewFinder(); - }, [goToQRCodeViewFinder, selfClient]); - // Prevents back navigation usePreventRemove(true, () => {}); const { bottom } = useSafeAreaInsets(); @@ -153,6 +120,10 @@ const HomeScreen: React.FC = () => { { + selfClient.trackEvent(DocumentEvents.DOCUMENT_SELECTED, { + document_type: documentData.data.documentType, + document_category: documentData.data.documentCategory, + }); setIdDetailsDocumentId(metadata.id); navigation.navigate('IdDetails'); }} @@ -170,39 +141,4 @@ const HomeScreen: React.FC = () => { ); }; -const pressStyle = { - opacity: 1, - backgroundColor: 'transparent', - transform: [{ scale: 0.95 }], -} as const; - -function PrivacyNote() { - const { hasPrivacyNoteBeenDismissed } = useSettingStore(); - const onDisclaimerPress = useHapticNavigation('Disclaimer'); - - if (hasPrivacyNoteBeenDismissed) { - return null; - } - - return ( - - - - A note on protecting your privacy - - - ); -} - export default HomeScreen; - -const Card = styled(YStack, { - width: '100%', - - flexGrow: 0, - backgroundColor: slate800, - borderRadius: 8, - gap: 12, - alignItems: 'center', - padding: 20, -}); diff --git a/app/src/screens/home/IdDetailsScreen.tsx b/app/src/screens/home/IdDetailsScreen.tsx index d94600b84..69c4381ce 100644 --- a/app/src/screens/home/IdDetailsScreen.tsx +++ b/app/src/screens/home/IdDetailsScreen.tsx @@ -9,7 +9,7 @@ import { BlurView } from '@react-native-community/blur'; import { useNavigation } from '@react-navigation/native'; import { PassportData } from '@selfxyz/common/types'; -import { DocumentCatalog } from '@selfxyz/common/utils/types'; +import { DocumentCatalog, IDDocument } from '@selfxyz/common/utils/types'; import IdCardLayout from '@/components/homeScreen/idCard'; import { usePassport } from '@/providers/passportDataProvider'; @@ -29,7 +29,7 @@ const IdDetailsScreen: React.FC = () => { const documentId = idDetailsDocumentId; const { getAllDocuments, loadDocumentCatalog, setSelectedDocument } = usePassport(); - const [document, setDocument] = useState(null); + const [document, setDocument] = useState(null); const [documentCatalog, setDocumentCatalog] = useState({ documents: [], }); diff --git a/app/src/screens/prove/ConfirmBelongingScreen.tsx b/app/src/screens/prove/ConfirmBelongingScreen.tsx index 360d64736..2b142cf47 100644 --- a/app/src/screens/prove/ConfirmBelongingScreen.tsx +++ b/app/src/screens/prove/ConfirmBelongingScreen.tsx @@ -8,7 +8,7 @@ import { ActivityIndicator, View } from 'react-native'; import type { StaticScreenProps } from '@react-navigation/native'; import { usePreventRemove } from '@react-navigation/native'; -import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; +import { loadSelectedDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { PassportEvents, ProofEvents, @@ -46,7 +46,22 @@ const ConfirmBelongingScreen: React.FC = () => { const isReadyToProve = currentState === 'ready_to_prove'; useEffect(() => { notificationSuccess(); - init(selfClient, 'dsc'); + + const initializeProving = async () => { + try { + const selectedDocument = await loadSelectedDocument(selfClient); + if (selectedDocument?.data?.documentCategory === 'aadhaar') { + init(selfClient, 'register'); + } else { + init(selfClient, 'dsc'); + } + } catch (error) { + console.error('Error loading selected document:', error); + init(selfClient, 'dsc'); + } + }; + + initializeProving(); }, [init, selfClient]); const onOkPress = async () => { @@ -109,10 +124,10 @@ const ConfirmBelongingScreen: React.FC = () => { > Confirm your identity - By continuing, you certify that this passport belongs to you and is - not stolen or forged. Once registered with Self, this document will - be permanently linked to your identity and can't be linked to - another one. + By continuing, you certify that this passport, biometric ID or + Aadhaar card belongs to you and is not stolen or forged. Once + registered with Self, this document will be permanently linked to + your identity and can't be linked to another one. { - - - Cancel - diff --git a/app/src/screens/recovery/AccountRecoveryChoiceScreen.tsx b/app/src/screens/recovery/AccountRecoveryChoiceScreen.tsx index 4247c5182..da3822ae5 100644 --- a/app/src/screens/recovery/AccountRecoveryChoiceScreen.tsx +++ b/app/src/screens/recovery/AccountRecoveryChoiceScreen.tsx @@ -66,6 +66,9 @@ const AccountRecoveryChoiceScreen: React.FC = () => { return useProtocolStore.getState()[docCategory].commitment_tree; }, getAltCSCA(docCategory) { + if (passportData.documentCategory === 'aadhaar') { + return useProtocolStore.getState().aadhaar.public_keys; + } return useProtocolStore.getState()[docCategory].alternative_csca; }, }, diff --git a/app/src/screens/recovery/AccountVerifiedSuccessScreen.tsx b/app/src/screens/recovery/AccountVerifiedSuccessScreen.tsx index 4b5305ac6..0d6724476 100644 --- a/app/src/screens/recovery/AccountVerifiedSuccessScreen.tsx +++ b/app/src/screens/recovery/AccountVerifiedSuccessScreen.tsx @@ -45,7 +45,7 @@ const AccountVerifiedSuccessScreen: React.FC = ({}) => { > ID Verified - Your passport information is now protected by Self ID. Just scan a + Your document's information is now protected by Self ID. Just scan a participating partner's QR code to prove your identity. diff --git a/app/src/screens/recovery/RecoverWithPhraseScreen.tsx b/app/src/screens/recovery/RecoverWithPhraseScreen.tsx index 668beb624..0c94719ab 100644 --- a/app/src/screens/recovery/RecoverWithPhraseScreen.tsx +++ b/app/src/screens/recovery/RecoverWithPhraseScreen.tsx @@ -71,6 +71,9 @@ const RecoverWithPhraseScreen: React.FC = () => { return useProtocolStore.getState()[docCategory].commitment_tree; }, getAltCSCA(docCategory) { + if (docCategory === 'aadhaar') { + return useProtocolStore.getState()[docCategory].public_keys; + } return useProtocolStore.getState()[docCategory].alternative_csca; }, }, diff --git a/app/src/screens/settings/ManageDocumentsScreen.tsx b/app/src/screens/settings/ManageDocumentsScreen.tsx index badff03b6..52d6ad6a6 100644 --- a/app/src/screens/settings/ManageDocumentsScreen.tsx +++ b/app/src/screens/settings/ManageDocumentsScreen.tsx @@ -114,6 +114,10 @@ const PassportDataSelector = () => { return 'ID Card'; case 'mock_id_card': return 'Mock ID Card'; + case 'aadhaar': + return 'Aadhaar'; + case 'mock_aadhaar': + return 'Mock Aadhaar'; default: return documentType; } @@ -288,6 +292,12 @@ const ManageDocumentsScreen: React.FC = () => { navigation.navigate('CreateMock'); }; + const handleAddAadhaar = () => { + impactLight(); + trackEvent(DocumentEvents.ADD_NEW_AADHAAR_SELECTED); + navigation.navigate('AadhaarUpload'); + }; + return ( { Scan New ID Document + + Add Aadhaar + Generate Mock Document diff --git a/app/src/screens/system/LaunchScreen.tsx b/app/src/screens/system/LaunchScreen.tsx index 42760f9d1..fcc8f1895 100644 --- a/app/src/screens/system/LaunchScreen.tsx +++ b/app/src/screens/system/LaunchScreen.tsx @@ -24,6 +24,7 @@ import { advercase, dinot } from '@/utils/fonts'; const LaunchScreen: React.FC = () => { useConnectionModal(); const onStartPress = useHapticNavigation('DocumentOnboarding'); + const onAadhaarPress = useHapticNavigation('AadhaarUpload'); const createMock = useHapticNavigation('CreateMock'); const { bottom } = useSafeAreaInsets(); @@ -37,7 +38,6 @@ const LaunchScreen: React.FC = () => { { Get started - Register with Self using your passport or biometric ID to prove your - identity across the web without revealing your personal information. + Register with Self using your passport, biometric ID or Aadhaar card + to prove your identity across the web without revealing your + personal information. - + { { > I have a Passport or Biometric ID + + I have an Aadhaar Card + @@ -109,15 +125,15 @@ export default LaunchScreen; const styles = StyleSheet.create({ container: { - flex: 1, + flex: 0, justifyContent: 'flex-start', alignItems: 'center', width: '102%', - paddingTop: '25%', + paddingTop: '30%', }, card: { width: '100%', - marginTop: '30%', + marginTop: '20%', borderRadius: 16, paddingVertical: 40, paddingHorizontal: 20, @@ -128,7 +144,7 @@ const styles = StyleSheet.create({ shadowOpacity: 0.2, shadowRadius: 12, elevation: 8, - marginBottom: 20, + marginBottom: 8, }, logoSection: { width: 60, diff --git a/app/src/utils/email.ts b/app/src/utils/email.ts index 06de608e7..412f15e59 100644 --- a/app/src/utils/email.ts +++ b/app/src/utils/email.ts @@ -25,7 +25,7 @@ export const sendCountrySupportNotification = async ({ countryCode, documentCategory, subject = `Country Support Request: ${countryName}`, - recipient = 'team@self.xyz', + recipient = 'support@self.xyz', }: SendCountrySupportNotificationOptions): Promise => { const deviceInfo = [ ['device', `${Platform.OS}@${Platform.Version}`], @@ -82,7 +82,7 @@ export const sendFeedbackEmail = async ({ message, origin, subject = 'SELF App Feedback', - recipient = 'team@self.xyz', + recipient = 'support@self.xyz', }: SendFeedbackEmailOptions): Promise => { const deviceInfo = [ ['device', `${Platform.OS}@${Platform.Version}`], diff --git a/app/src/utils/proving/provingMachine.ts b/app/src/utils/proving/provingMachine.ts index 6e094bee4..674f3f765 100644 --- a/app/src/utils/proving/provingMachine.ts +++ b/app/src/utils/proving/provingMachine.ts @@ -41,6 +41,7 @@ import { getPayload, getWSDbRelayerUrl, } from '@selfxyz/common/utils/proving'; +import type { IDDocument } from '@selfxyz/common/utils/types'; import type { SelfClient } from '@selfxyz/mobile-sdk-alpha'; import { clearPassportData, @@ -72,10 +73,20 @@ const getMappingKey = ( documentCategory: DocumentCategory, ): string => { if (circuitType === 'disclose') { - return documentCategory === 'passport' ? 'DISCLOSE' : 'DISCLOSE_ID'; + if (documentCategory === 'passport') return 'DISCLOSE'; + if (documentCategory === 'id_card') return 'DISCLOSE_ID'; + if (documentCategory === 'aadhaar') return 'DISCLOSE_AADHAAR'; + throw new Error( + `Unsupported document category for disclose: ${documentCategory}`, + ); } if (circuitType === 'register') { - return documentCategory === 'passport' ? 'REGISTER' : 'REGISTER_ID'; + if (documentCategory === 'passport') return 'REGISTER'; + if (documentCategory === 'id_card') return 'REGISTER_ID'; + if (documentCategory === 'aadhaar') return 'REGISTER_AADHAAR'; + throw new Error( + `Unsupported document category for register: ${documentCategory}`, + ); } // circuitType === 'dsc' return documentCategory === 'passport' ? 'DSC' : 'DSC_ID'; @@ -95,10 +106,10 @@ const resolveWebSocketUrl = ( }; // Helper functions for _generatePayload refactoring -const _generateCircuitInputs = ( +const _generateCircuitInputs = async ( circuitType: 'disclose' | 'register' | 'dsc', secret: string | undefined | null, - passportData: PassportData, + passportData: IDDocument, env: 'prod' | 'stg', ) => { const document: DocumentCategory = passportData.documentCategory; @@ -114,17 +125,19 @@ const _generateCircuitInputs = ( switch (circuitType) { case 'register': ({ inputs, circuitName, endpointType, endpoint } = - generateTEEInputsRegister( + await generateTEEInputsRegister( secret as string, passportData, - protocolStore[document].dsc_tree, + document === 'aadhaar' + ? protocolStore[document].public_keys + : protocolStore[document].dsc_tree, env, )); circuitTypeWithDocumentExtension = `${circuitType}${document === 'passport' ? '' : '_id'}`; break; case 'dsc': ({ inputs, circuitName, endpointType, endpoint } = generateTEEInputsDSC( - passportData, + passportData as PassportData, protocolStore[document].csca_tree as string[][], env, )); @@ -140,7 +153,9 @@ const _generateCircuitInputs = ( const docStore = doc === 'passport' ? protocolStore.passport - : protocolStore.id_card; + : doc === 'aadhaar' + ? protocolStore.aadhaar + : protocolStore.id_card; switch (tree) { case 'ofac': return docStore.ofac_trees; @@ -335,7 +350,7 @@ interface ProvingState { socketConnection: Socket | null; uuid: string | null; userConfirmed: boolean; - passportData: PassportData | null; + passportData: IDDocument | null; secret: string | null; circuitType: provingMachineCircuitType | null; error_code: string | null; @@ -959,34 +974,49 @@ export const useProvingStore = create((set, get) => { selfClient.trackEvent(ProofEvents.FETCH_DATA_STARTED); const startTime = Date.now(); const context = createProofContext('startFetchingData'); + + // passport and id card logProofEvent('info', 'Fetching DSC data started', context); try { const { passportData, env } = get(); if (!passportData) { throw new Error('PassportData is not available'); } - if (!passportData?.dsc_parsed) { - logProofEvent('error', 'Missing parsed DSC', context, { - failure: 'PROOF_FAILED_DATA_FETCH', - duration_ms: Date.now() - startTime, - }); - console.error('Missing parsed DSC in passport data'); - selfClient.trackEvent(ProofEvents.FETCH_DATA_FAILED, { - message: 'Missing parsed DSC in passport data', - }); - actor!.send({ type: 'FETCH_ERROR' }); - return; - } const document: DocumentCategory = passportData.documentCategory; - logProofEvent('info', 'Protocol store fetch', context, { - step: 'protocol_store_fetch', - document, - }); - await useProtocolStore - .getState() - [ - document - ].fetch_all(env!, (passportData as PassportData).dsc_parsed!.authorityKeyIdentifier); + console.log('document', document); + switch (passportData.documentCategory) { + case 'passport': + case 'id_card': + if (!passportData?.dsc_parsed) { + logProofEvent('error', 'Missing parsed DSC', context, { + failure: 'PROOF_FAILED_DATA_FETCH', + duration_ms: Date.now() - startTime, + }); + console.error('Missing parsed DSC in passport data'); + selfClient.trackEvent(ProofEvents.FETCH_DATA_FAILED, { + message: 'Missing parsed DSC in passport data', + }); + actor!.send({ type: 'FETCH_ERROR' }); + return; + } + logProofEvent('info', 'Protocol store fetch', context, { + step: 'protocol_store_fetch', + document, + }); + await useProtocolStore + .getState() + [ + document + ].fetch_all(env!, (passportData as PassportData).dsc_parsed!.authorityKeyIdentifier); + break; + case 'aadhaar': + logProofEvent('info', 'Protocol store fetch', context, { + step: 'protocol_store_fetch', + document, + }); + await useProtocolStore.getState()[document].fetch_all(env!); + break; + } logProofEvent('info', 'Data fetch succeeded', context, { duration_ms: Date.now() - startTime, }); @@ -1084,8 +1114,10 @@ export const useProvingStore = create((set, get) => { secret as string, { getCommitmentTree, - getAltCSCA: docType => - useProtocolStore.getState()[docType].alternative_csca, + getAltCSCA: (docType: DocumentCategory) => + docType === 'aadhaar' + ? useProtocolStore.getState().aadhaar.public_keys + : useProtocolStore.getState()[docType].alternative_csca, }, ); logProofEvent( @@ -1135,16 +1167,18 @@ export const useProvingStore = create((set, get) => { return; } const document: DocumentCategory = passportData.documentCategory; - const isDscRegistered = await checkIfPassportDscIsInTree( - passportData, - useProtocolStore.getState()[document].dsc_tree, - ); - logProofEvent('info', 'DSC tree check', context, { - dsc_registered: isDscRegistered, - }); - if (isDscRegistered) { - selfClient.trackEvent(ProofEvents.DSC_IN_TREE); - set({ circuitType: 'register' }); + if (document === 'passport' || document === 'id_card') { + const isDscRegistered = await checkIfPassportDscIsInTree( + passportData, + useProtocolStore.getState()[document].dsc_tree, + ); + logProofEvent('info', 'DSC tree check', context, { + dsc_registered: isDscRegistered, + }); + if (isDscRegistered) { + selfClient.trackEvent(ProofEvents.DSC_IN_TREE); + set({ circuitType: 'register' }); + } } logProofEvent('info', 'Validation succeeded', context, { duration_ms: Date.now() - startTime, @@ -1183,7 +1217,10 @@ export const useProvingStore = create((set, get) => { let circuitName; if (circuitType === 'disclose') { - circuitName = 'disclose'; + circuitName = + passportData.documentCategory === 'aadhaar' + ? 'disclose_aadhaar' + : 'disclose'; } else { circuitName = getCircuitNameFromPassportData( passportData, @@ -1196,6 +1233,7 @@ export const useProvingStore = create((set, get) => { passportData as PassportData, circuitName, ); + logProofEvent('info', 'Circuit resolution', baseContext, { circuit_name: circuitName, ws_url: wsRpcUrl, @@ -1433,7 +1471,7 @@ export const useProvingStore = create((set, get) => { endpointType, endpoint, circuitTypeWithDocumentExtension, - } = _generateCircuitInputs( + } = await _generateCircuitInputs( circuitType as 'disclose' | 'register' | 'dsc', secret, passportData, @@ -1499,7 +1537,11 @@ export const useProvingStore = create((set, get) => { _handlePassportNotSupported: (selfClient: SelfClient) => { const passportData = get().passportData; - const countryCode = passportData?.passportMetadata?.countryCode; + + const countryCode = + passportData?.documentCategory !== 'aadhaar' + ? (passportData as PassportData)?.passportMetadata?.countryCode + : 'IND'; const documentCategory = passportData?.documentCategory; selfClient.emit(SdkEvents.PROVING_PASSPORT_NOT_SUPPORTED, { diff --git a/app/src/utils/qrScanner.ts b/app/src/utils/qrScanner.ts new file mode 100644 index 000000000..3e3048e41 --- /dev/null +++ b/app/src/utils/qrScanner.ts @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +import { NativeModules, Platform } from 'react-native'; + +interface QRScannerBridge { + scanQRCode: () => Promise; + scanQRCodeFromPhotoLibrary: () => Promise; +} + +// Platform-specific QRScanner implementation +let QRScanner: QRScannerBridge | null = null; + +if (Platform.OS === 'ios') { + QRScanner = NativeModules.QRScannerBridge || null; +} else if (Platform.OS === 'android') { + QRScanner = NativeModules.QRCodeScanner || null; +} else { + console.warn('QRScanner: Unsupported platform'); + QRScanner = null; +} + +export { QRScanner }; + +/** + * Check if QR scanner camera is available + */ +export const isQRScannerCameraAvailable = (): boolean => { + return QRScanner?.scanQRCode != null; +}; + +/** + * Check if QR scanner photo library is available + */ +export const isQRScannerPhotoLibraryAvailable = (): boolean => { + return QRScanner?.scanQRCodeFromPhotoLibrary != null; +}; + +/** + * Scans QR code from photo library + * @returns Promise that resolves with the QR code content + */ +export const scanQRCodeFromPhotoLibrary = async (): Promise => { + if (!QRScanner?.scanQRCodeFromPhotoLibrary) { + throw new Error('QR Scanner photo library not available on this platform'); + } + + return await QRScanner.scanQRCodeFromPhotoLibrary(); +}; + +/** + * Scans QR code using device camera + * @returns Promise that resolves with the QR code content + */ +export const scanQRCodeWithCamera = async (): Promise => { + if (!QRScanner?.scanQRCode) { + throw new Error('QR Scanner not available on this platform'); + } + + return await QRScanner.scanQRCode(); +}; diff --git a/app/tests/src/navigation.test.ts b/app/tests/src/navigation.test.ts index d885498cd..915e76839 100644 --- a/app/tests/src/navigation.test.ts +++ b/app/tests/src/navigation.test.ts @@ -7,6 +7,7 @@ describe('navigation', () => { const navigationScreens = require('@/navigation').navigationScreens; const listOfScreens = Object.keys(navigationScreens).sort(); expect(listOfScreens).toEqual([ + 'AadhaarUpload', 'AccountRecovery', 'AccountRecoveryChoice', 'AccountVerifiedSuccess', diff --git a/common/index.ts b/common/index.ts index 2bf8677f7..a7174d797 100644 --- a/common/index.ts +++ b/common/index.ts @@ -1,5 +1,6 @@ // Type exports from constants export type { + AadhaarData, CertificateData, DocumentCategory, IdDocInput, @@ -84,14 +85,8 @@ export { stringToBigInt, } from './src/utils/index.js'; -export { - prepareAadhaarRegisterTestData, - prepareAadhaarDiscloseTestData, - prepareAadhaarRegisterData, - prepareAadhaarDiscloseData, -} from './src/utils/aadhaar/mockData.js'; -export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js'; export { createSelector } from './src/utils/aadhaar/constants.js'; + // Hash utilities export { customHasher, @@ -100,3 +95,14 @@ export { hash, packBytesAndPoseidon, } from './src/utils/hash.js'; + +export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js'; + +export { isAadhaarDocument, isMRZDocument } from './src/utils/index.js'; + +export { + prepareAadhaarDiscloseData, + prepareAadhaarDiscloseTestData, + prepareAadhaarRegisterData, + prepareAadhaarRegisterTestData, +} from './src/utils/aadhaar/mockData.js'; diff --git a/common/src/constants/constants.ts b/common/src/constants/constants.ts index d63972ec5..e93723b00 100644 --- a/common/src/constants/constants.ts +++ b/common/src/constants/constants.ts @@ -97,6 +97,11 @@ export const IDENTITY_TREE_URL_STAGING = 'https://tree.staging.self.xyz/identity export const IDENTITY_TREE_URL_STAGING_ID_CARD = 'https://tree.staging.self.xyz/identity-id'; +export const IDENTITY_VERIFICATION_HUB_ADDRESS = '0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF'; + +export const IDENTITY_VERIFICATION_HUB_ADDRESS_STAGING = + '0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74'; + export const ID_CARD_ATTESTATION_ID = '2'; export const MAX_BYTES_IN_FIELD = 31; diff --git a/common/src/mock_certificates/aadhaar/mockAadhaarCert.ts b/common/src/mock_certificates/aadhaar/mockAadhaarCert.ts new file mode 100644 index 000000000..6a3b27562 --- /dev/null +++ b/common/src/mock_certificates/aadhaar/mockAadhaarCert.ts @@ -0,0 +1,56 @@ +// Hardcoded Aadhaar test certificates for development purposes only +// This file contains mock private keys that should NEVER be used in production + +export const AADHAAR_MOCK_PRIVATE_KEY_PEM = `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC//2Yjq4TpEm1t +5Fm4MM/+8MhGPd9vTAZpo04L7HYFbe4YdFmXZBLXH6KbLrbK3uhMuq9dmotJiDtx +Wjch5f5iHwqLLUKsSHJl4Mr2eFZj77TTLkxTEUYEISpRm9JSIHYRg7kcFPbR+CrE +uAe9s3/qLDAD85gqDCiosj6bCovMLayHQYglqN2pbYNp8ZIFaVj1gdkoQg8wCK5O +D3jy5CJnvJirNuiWrvdRLZ48o01L7b/2B/iuBWtoBtOaCTPWZutBIcKB6oNUKBbY +zwG40NxWpQtAeY6NW0CC/apqUEZVPLdYijjsLGBUohHTtLCXB/C1KDNh0sNTfMU8 +bkctLqvXAgMBAAECggEAD3zqgBS6F1RRhOyUR9VfZskepsfr9ve/ieFodNhhpuUS +Y8efyIrqmCiPPr+npp+q4DGsRTunyJbXdx8YO0EcSOcIvAE6xr7ekS68JxWLBoT6 +MpG8CqfMkAQeFh1trte7UbgtN3SbeoTV6/uNqE7LRUuRbgHGM+VTzFP6OxomyW5/ +BGHmhlU5j5r4gdNrztwpnfLFZvZt+4yR99kWIoYbFAvgq6sgRGflo45dHGG80TUd +o3vir1IeNAY5vkeJ9owCxUJW4JxJKarjlBibqRUprEgnjKr2ovxirjUOzOClmVEJ +tgyx4doY/F9cE8jD4JfcC7xxC79j90odfEED+5IBKQKBgQD2nCwPxr9YxMiQLQii +Z4E7x96nHdTvqXKSTPGWX2Zv6Sur9qL1Wyz30tt3COB7+b9UwtpTozDxxlVn+u4U +SnDdVWMrUpDi03dRvsLWhTDC8btN6WnYqGmHKSjst+yHytPo39cqQVZ4TY8Gqfg0 +3/Pqb5hpxkJ2RRxVt3gDvgnOPwKBgQDHTuSxZbpQ956z8t6BA4tDYkFC9BFQpb/F +pSrw2w8PMZH4QckbjFj59ME2u/WLyuJ+U8GjR4YTk8ZXQ5niSrPDC5Pa6s3Ano44 +8h5FgrMeAbxZ0HuANHRS1YWba8k4tbeunAdj08nIviMJEuhMcjzbqgf5rFrrGzR5 +Jb86eznsaQKBgBzMLeUFu3B9QkJ7z8dPOOsnMtvnAuedrPBipc9+gnLNErl5CpyG +MiEacWBcHAK+LlaSjnY3105Ub8K9rbGW48kk4Hi9ooeqVAOquAve78vD+LBncmHH +gNM0vj+uVqOgztAh23lmuddAj1Qi4wYhpNUahPzNFxPCjEWCMDSXq4N3AoGAcqR1 +tXi/WA1m8zkzNWCVfXgJ8/ox74K3sXdVIN/QZLvtq7Ajfr4W/AgGD3bEQdm8uE9z +JXlhrOcmglF3NYwkpH+HV7gSC8boJedW9EK+xvbWoY7jSxZhBridNo4kW4NjGYPU +WF6dRePggTqn9jkLuoquNbYnQe8PGtRUj84LvmkCgYEA5iu39qcCBd+JuPDmxOJx +Ah3QcOFI4i7WW9oi7+68aCqee9K7d659hyYWpewYcDXzvSLYvXJJcU9vkVuW8DLK +lQzKVNh2/5SaAN/EysYBpFQVbNZ5dA74WrjxnPsNmwRc6yv/o8I/LfgWOB9yB3fI +avCtlYniKHPvSCA/gS2h4fk= +-----END PRIVATE KEY----- +`; + +export const AADHAAR_MOCK_PUBLIC_KEY_PEM = `-----BEGIN CERTIFICATE----- +MIIDjzCCAnegAwIBAgIUZA6u4qBxEjW4dxmbLaLkWnHIybowDQYJKoZIhvcNAQEL +BQAwVzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 +MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0y +NTA5MTgxMDE2NTlaFw0yNjA5MTgxMDE2NTlaMFcxCzAJBgNVBAYTAlVTMQ4wDAYD +VQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UECgwMT3JnYW5pemF0aW9u +MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC//2Yjq4TpEm1t5Fm4MM/+8MhGPd9vTAZpo04L7HYFbe4YdFmXZBLXH6Kb +LrbK3uhMuq9dmotJiDtxWjch5f5iHwqLLUKsSHJl4Mr2eFZj77TTLkxTEUYEISpR +m9JSIHYRg7kcFPbR+CrEuAe9s3/qLDAD85gqDCiosj6bCovMLayHQYglqN2pbYNp +8ZIFaVj1gdkoQg8wCK5OD3jy5CJnvJirNuiWrvdRLZ48o01L7b/2B/iuBWtoBtOa +CTPWZutBIcKB6oNUKBbYzwG40NxWpQtAeY6NW0CC/apqUEZVPLdYijjsLGBUohHT +tLCXB/C1KDNh0sNTfMU8bkctLqvXAgMBAAGjUzBRMB0GA1UdDgQWBBTGyVMLFNL2 +PRJwtA8vekrtJVu2BTAfBgNVHSMEGDAWgBTGyVMLFNL2PRJwtA8vekrtJVu2BTAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCwcKlyaZw3jxDNtU6j +V8g9tUr77z0LyTrVe0GujxFaa4EKKKqG/lzf6wNDaHGOgyEwhPsi/ui8VU6Y8KTS +SxorUta+2zNHu8jziz1rxYTfgPWvK54B3Q3q4ycRLmYfR0CVvH2+TvTAqfEEvpEh +8tY9mpNzjYsLzlwPszkWU+WpLJjH0VPhVIiFC65EaxuArZrap8IpuK/bSa4Beqbb +7rMo/KmDfhFpVMQcOrvyQJmurtmjo12Esb0EjwZp634nDVRC9gFXEh5YuWBg3IaI +cTCvHQ+MAXTzZMOfc2dWZYdk1PaO6xLTw0YfGAtl6r3x4Csd0i5iwpDo1JXjSpZE +mESQ +-----END CERTIFICATE----- +`; diff --git a/common/src/utils/aadhaar/mockData.ts b/common/src/utils/aadhaar/mockData.ts index e6caa3e19..78850d9b6 100644 --- a/common/src/utils/aadhaar/mockData.ts +++ b/common/src/utils/aadhaar/mockData.ts @@ -1,8 +1,11 @@ import forge from 'node-forge'; import { poseidon5 } from 'poseidon-lite'; + import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js'; +import { formatCountriesList } from '../circuits/formatInputs.js'; import { findIndexInTree, formatInput } from '../circuits/generateInputs.js'; import { packBytesAndPoseidon } from '../hash.js'; +import { shaPad } from '../shaPad.js'; import { generateMerkleProof, generateSMTProof, @@ -10,20 +13,25 @@ import { getNameYobLeafAahaar, } from '../trees.js'; import { testQRData } from './assets/dataInput.js'; -import { calculateAge, generateTestData, stringToAsciiArray, testCustomData } from './utils.js'; -import { extractQRDataFields } from './utils.js'; import { AadhaarField, createSelector } from './constants.js'; -import { formatCountriesList } from '../circuits/formatInputs.js'; -import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; -import { SMT } from '@openpassport/zk-kit-smt'; +import { + calculateAge, + extractQRDataFields, + generateTestData, + stringToAsciiArray, + testCustomData, +} from './utils.js'; + import { convertBigIntToByteArray, decompressByteArray, extractPhoto, splitToWords, } from '@anon-aadhaar/core'; -import { sha256Pad } from '@zk-email/helpers/dist/sha-utils.js'; +import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; +import { SMT } from '@openpassport/zk-kit-smt'; import { bufferToHex, Uint8ArrayToCharArray } from '@zk-email/helpers/dist/binary-format.js'; +import { sha256Pad } from '@zk-email/helpers/dist/sha-utils.js'; // Helper function to compute padded name function computePaddedName(name: string): number[] { @@ -41,22 +49,18 @@ function computeUppercasePaddedName(name: string): number[] { .map((char) => char.charCodeAt(0)); } -// Helper function to compute nullifier -function nullifierHash(extractedFields: ReturnType): bigint { - const genderAscii = stringToAsciiArray(extractedFields.gender)[0]; - const personalInfoHashArgs = [ - genderAscii, - ...stringToAsciiArray(extractedFields.yob), - ...stringToAsciiArray(extractedFields.mob), - ...stringToAsciiArray(extractedFields.dob), - ...stringToAsciiArray(extractedFields.name.toUpperCase().padEnd(62, '\0')), - ...stringToAsciiArray(extractedFields.aadhaarLast4Digits), - ]; - return BigInt(packBytesAndPoseidon(personalInfoHashArgs)); +export function convertByteArrayToBigInt(byteArray: Uint8Array | number[]): bigint { + let result = 0n; + for (let i = 0; i < byteArray.length; i++) { + result = result * 256n + BigInt(byteArray[i]); + } + return result; } // Helper function to compute packed commitment -function computePackedCommitment(extractedFields: ReturnType): bigint { +export function computePackedCommitment( + extractedFields: ReturnType +): bigint { const packedCommitmentArgs = [ 3, ...stringToAsciiArray(extractedFields.pincode), @@ -68,7 +72,7 @@ function computePackedCommitment(extractedFields: ReturnType): bigint { + const genderAscii = stringToAsciiArray(extractedFields.gender)[0]; + const personalInfoHashArgs = [ + genderAscii, + ...stringToAsciiArray(extractedFields.yob), + ...stringToAsciiArray(extractedFields.mob), + ...stringToAsciiArray(extractedFields.dob), + ...stringToAsciiArray(extractedFields.name.toUpperCase().padEnd(62, '\0')), + ...stringToAsciiArray(extractedFields.aadhaarLast4Digits), + ]; + return BigInt(packBytesAndPoseidon(personalInfoHashArgs)); } -function processQRDataSimple(qrData: string) { +export function processQRDataSimple(qrData: string) { const qrDataBytes = convertBigIntToByteArray(BigInt(qrData)); const decodedData = decompressByteArray(qrDataBytes); const signedData = decodedData.slice(0, decodedData.length - 256); - - const [qrDataPadded, qrDataPaddedLen] = sha256Pad(signedData, 512 * 3); - + const [qrDataPaddedNumber, qrDataPaddedLen] = shaPad(signedData, 512 * 3); + const qrDataPadded = new Uint8Array(qrDataPaddedNumber); let photoEOI = 0; for (let i = 0; i < qrDataPadded.length - 1; i++) { if (qrDataPadded[i + 1] === 217 && qrDataPadded[i] === 255) { @@ -163,19 +145,161 @@ function processQRDataSimple(qrData: string) { }; } -export function prepareAadhaarRegisterTestData( - privKeyPem: string, - pubkeyPem: string, +export function prepareAadhaarDiscloseData( + qrData: string, + identityTree: LeanIMT, + nameAndDob_smt: SMT, + nameAndYob_smt: SMT, + scope: string, + secret: string, + user_identifier: string, + discloseAttributes: { + dateOfBirth?: boolean; + name?: boolean; + gender?: boolean; + idNumber?: boolean; + issuingState?: boolean; + minimumAge?: number; + forbiddenCountriesListPacked?: string[]; + ofac?: boolean; + } +) { + const sharedData = processQRDataSimple(qrData); + + const { currentYear, currentMonth, currentDay } = calculateAge( + sharedData.extractedFields.dob, + sharedData.extractedFields.mob, + sharedData.extractedFields.yob + ); + + const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0]; + const nullifier = nullifierHash(sharedData.extractedFields); + const packedCommitment = computePackedCommitment(sharedData.extractedFields); + const commitment = computeCommitment( + BigInt(secret), + BigInt(sharedData.qrHash), + nullifier, + packedCommitment, + BigInt(sharedData.photoHash) + ); + + const paddedName = computePaddedName(sharedData.extractedFields.name); + + const index = findIndexInTree(identityTree, BigInt(commitment)); + const { + siblings, + path: merkle_path, + leaf_depth, + } = generateMerkleProof(identityTree, index, COMMITMENT_TREE_DEPTH); + + const namedob_leaf = getNameDobLeafAadhaar( + sharedData.extractedFields.name, + sharedData.extractedFields.yob, + sharedData.extractedFields.mob, + sharedData.extractedFields.dob + ); + const nameyob_leaf = getNameYobLeafAahaar( + sharedData.extractedFields.name, + sharedData.extractedFields.yob + ); + + const { + root: ofac_name_dob_smt_root, + closestleaf: ofac_name_dob_smt_leaf_key, + siblings: ofac_name_dob_smt_siblings, + } = generateSMTProof(nameAndDob_smt, namedob_leaf); + + const { + root: ofac_name_yob_smt_root, + closestleaf: ofac_name_yob_smt_leaf_key, + siblings: ofac_name_yob_smt_siblings, + } = generateSMTProof(nameAndYob_smt, nameyob_leaf); + + const selectorArr: AadhaarField[] = []; + if (discloseAttributes.dateOfBirth) { + selectorArr.push('YEAR_OF_BIRTH'); + selectorArr.push('MONTH_OF_BIRTH'); + selectorArr.push('DAY_OF_BIRTH'); + } + if (discloseAttributes.name) { + selectorArr.push('NAME'); + } + if (discloseAttributes.gender) { + selectorArr.push('GENDER'); + } + if (discloseAttributes.idNumber) { + selectorArr.push('AADHAAR_LAST_4_DIGITS'); + } + if (discloseAttributes.issuingState) { + selectorArr.push('STATE'); + } + if (discloseAttributes.ofac) { + selectorArr.push('OFAC_NAME_DOB_CHECK'); + selectorArr.push('OFAC_NAME_YOB_CHECK'); + } + + const selector = createSelector(selectorArr); + + const inputs = { + attestation_id: '3', + secret, + qrDataHash: formatInput(BigInt(sharedData.qrHash)), + gender: formatInput(genderAscii), + // qrDataHash: BigInt(sharedData.qrHash).toString(), + // gender: genderAscii.toString(), + yob: stringToAsciiArray(sharedData.extractedFields.yob), + mob: stringToAsciiArray(sharedData.extractedFields.mob), + dob: stringToAsciiArray(sharedData.extractedFields.dob), + name: formatInput(paddedName), + aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits), + pincode: stringToAsciiArray(sharedData.extractedFields.pincode), + state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')), + ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits), + photoHash: formatInput(BigInt(sharedData.photoHash)), + merkle_root: formatInput(BigInt(identityTree.root)), + leaf_depth: formatInput(leaf_depth), + path: formatInput(merkle_path), + siblings: formatInput(siblings), + ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)), + ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)), + ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings), + ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)), + ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)), + ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings), + selector: formatInput(selector), + minimumAge: formatInput(discloseAttributes.minimumAge ?? 0), + currentYear: formatInput(currentYear), + currentMonth: formatInput(currentMonth), + currentDay: formatInput(currentDay), + scope: formatInput(BigInt(scope)), + user_identifier: formatInput(BigInt(user_identifier)), + forbidden_countries_list: discloseAttributes.forbiddenCountriesListPacked + ? formatInput(formatCountriesList(discloseAttributes.forbiddenCountriesListPacked)) + : formatInput([...Array(120)].map((_) => '0')), + }; + + return inputs; +} + +export function prepareAadhaarDiscloseTestData( + privateKeyPem: string, + merkletree: LeanIMT, + nameAndDob_smt: SMT, + nameAndYob_smt: SMT, + scope: string, secret: string, + user_identifier: string, + selector: string, name?: string, dateOfBirth?: string, gender?: string, pincode?: string, state?: string, - timestamp?: string + timestamp?: string, + updateTree?: boolean ) { const sharedData = processQRData( - privKeyPem, + privateKeyPem, name, dateOfBirth, gender, @@ -184,36 +308,13 @@ export function prepareAadhaarRegisterTestData( timestamp ); - const delimiterIndices: number[] = []; - for (let i = 0; i < sharedData.qrDataPadded.length; i++) { - if (sharedData.qrDataPadded[i] === 255) { - delimiterIndices.push(i); - } - if (delimiterIndices.length === 18) { - break; - } - } - let photoEOI = 0; - for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) { - if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) { - photoEOI = i + 1; - } - } - if (photoEOI === 0) { - throw new Error('Photo EOI not found'); - } - - const signatureBytes = sharedData.decodedData.slice( - sharedData.decodedData.length - 256, - sharedData.decodedData.length + const { age, currentYear, currentMonth, currentDay } = calculateAge( + sharedData.extractedFields.dob, + sharedData.extractedFields.mob, + sharedData.extractedFields.yob ); - const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString()); - - const publicKey = forge.pki.publicKeyFromPem(pubkeyPem); - - const modulusHex = publicKey.n.toString(16); - const pubKey = BigInt('0x' + modulusHex); + const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0]; const nullifier = nullifierHash(sharedData.extractedFields); const packedCommitment = computePackedCommitment(sharedData.extractedFields); const commitment = computeCommitment( @@ -224,14 +325,74 @@ export function prepareAadhaarRegisterTestData( BigInt(sharedData.photoHash) ); - const inputs = { - qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded), - qrDataPaddedLength: sharedData.qrDataPaddedLen, - delimiterIndices: delimiterIndices, - signature: splitToWords(signature, BigInt(121), BigInt(17)), - pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)), - secret: secret, - photoEOI: photoEOI, + const paddedName = computePaddedName(sharedData.extractedFields.name); + + if (updateTree) { + merkletree.insert(BigInt(commitment)); + } + + const index = findIndexInTree(merkletree, BigInt(commitment)); + const { + siblings, + path: merkle_path, + leaf_depth, + } = generateMerkleProof(merkletree, index, COMMITMENT_TREE_DEPTH); + + const namedob_leaf = getNameDobLeafAadhaar( + sharedData.extractedFields.name, + sharedData.extractedFields.yob, + sharedData.extractedFields.mob, + sharedData.extractedFields.dob + ); + const nameyob_leaf = getNameYobLeafAahaar( + sharedData.extractedFields.name, + sharedData.extractedFields.yob + ); + + const { + root: ofac_name_dob_smt_root, + closestleaf: ofac_name_dob_smt_leaf_key, + siblings: ofac_name_dob_smt_siblings, + } = generateSMTProof(nameAndDob_smt, namedob_leaf); + + const { + root: ofac_name_yob_smt_root, + closestleaf: ofac_name_yob_smt_leaf_key, + siblings: ofac_name_yob_smt_siblings, + } = generateSMTProof(nameAndYob_smt, nameyob_leaf); + + const inputs = { + attestation_id: '3', + secret: secret, + qrDataHash: BigInt(sharedData.qrHash).toString(), + gender: genderAscii.toString(), + yob: stringToAsciiArray(sharedData.extractedFields.yob), + mob: stringToAsciiArray(sharedData.extractedFields.mob), + dob: stringToAsciiArray(sharedData.extractedFields.dob), + name: formatInput(paddedName), + aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits), + pincode: stringToAsciiArray(sharedData.extractedFields.pincode), + state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')), + ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits), + photoHash: formatInput(BigInt(sharedData.photoHash)), + merkle_root: formatInput(BigInt(merkletree.root)), + leaf_depth: formatInput(leaf_depth), + path: formatInput(merkle_path), + siblings: formatInput(siblings), + ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)), + ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)), + ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings), + ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)), + ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)), + ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings), + selector, + minimumAge: formatInput(age - 2), + currentYear: formatInput(currentYear), + currentMonth: formatInput(currentMonth), + currentDay: formatInput(currentDay), + scope: formatInput(BigInt(scope)), + user_identifier: formatInput(BigInt(user_identifier)), + forbidden_countries_list: [...Array(120)].map((x) => '0'), }; return { @@ -319,25 +480,19 @@ export async function prepareAadhaarRegisterData(qrData: string, secret: string, return inputs; } -export function prepareAadhaarDiscloseTestData( - privateKeyPem: string, - merkletree: LeanIMT, - nameAndDob_smt: SMT, - nameAndYob_smt: SMT, - scope: string, +export function prepareAadhaarRegisterTestData( + privKeyPem: string, + pubkeyPem: string, secret: string, - user_identifier: string, - selector: string, name?: string, dateOfBirth?: string, gender?: string, pincode?: string, state?: string, - timestamp?: string, - updateTree?: boolean + timestamp?: string ) { const sharedData = processQRData( - privateKeyPem, + privKeyPem, name, dateOfBirth, gender, @@ -346,13 +501,36 @@ export function prepareAadhaarDiscloseTestData( timestamp ); - const { age, currentYear, currentMonth, currentDay } = calculateAge( - sharedData.extractedFields.dob, - sharedData.extractedFields.mob, - sharedData.extractedFields.yob + const delimiterIndices: number[] = []; + for (let i = 0; i < sharedData.qrDataPadded.length; i++) { + if (sharedData.qrDataPadded[i] === 255) { + delimiterIndices.push(i); + } + if (delimiterIndices.length === 18) { + break; + } + } + let photoEOI = 0; + for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) { + if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) { + photoEOI = i + 1; + } + } + if (photoEOI === 0) { + throw new Error('Photo EOI not found'); + } + + const signatureBytes = sharedData.decodedData.slice( + sharedData.decodedData.length - 256, + sharedData.decodedData.length ); + const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString()); + + const publicKey = forge.pki.publicKeyFromPem(pubkeyPem); + + const modulusHex = publicKey.n.toString(16); + const pubKey = BigInt('0x' + modulusHex); - const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0]; const nullifier = nullifierHash(sharedData.extractedFields); const packedCommitment = computePackedCommitment(sharedData.extractedFields); const commitment = computeCommitment( @@ -363,74 +541,14 @@ export function prepareAadhaarDiscloseTestData( BigInt(sharedData.photoHash) ); - const paddedName = computePaddedName(sharedData.extractedFields.name); - - if (updateTree) { - merkletree.insert(BigInt(commitment)); - } - - const index = findIndexInTree(merkletree, BigInt(commitment)); - const { - siblings, - path: merkle_path, - leaf_depth, - } = generateMerkleProof(merkletree, index, COMMITMENT_TREE_DEPTH); - - const namedob_leaf = getNameDobLeafAadhaar( - sharedData.extractedFields.name, - sharedData.extractedFields.yob, - sharedData.extractedFields.mob, - sharedData.extractedFields.dob - ); - const nameyob_leaf = getNameYobLeafAahaar( - sharedData.extractedFields.name, - sharedData.extractedFields.yob - ); - - const { - root: ofac_name_dob_smt_root, - closestleaf: ofac_name_dob_smt_leaf_key, - siblings: ofac_name_dob_smt_siblings, - } = generateSMTProof(nameAndDob_smt, namedob_leaf); - - const { - root: ofac_name_yob_smt_root, - closestleaf: ofac_name_yob_smt_leaf_key, - siblings: ofac_name_yob_smt_siblings, - } = generateSMTProof(nameAndYob_smt, nameyob_leaf); - const inputs = { - attestation_id: '3', + qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded), + qrDataPaddedLength: sharedData.qrDataPaddedLen, + delimiterIndices: delimiterIndices, + signature: splitToWords(signature, BigInt(121), BigInt(17)), + pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)), secret: secret, - qrDataHash: BigInt(sharedData.qrHash).toString(), - gender: genderAscii.toString(), - yob: stringToAsciiArray(sharedData.extractedFields.yob), - mob: stringToAsciiArray(sharedData.extractedFields.mob), - dob: stringToAsciiArray(sharedData.extractedFields.dob), - name: formatInput(paddedName), - aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits), - pincode: stringToAsciiArray(sharedData.extractedFields.pincode), - state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')), - ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits), - photoHash: formatInput(BigInt(sharedData.photoHash)), - merkle_root: formatInput(BigInt(merkletree.root)), - leaf_depth: formatInput(leaf_depth), - path: formatInput(merkle_path), - siblings: formatInput(siblings), - ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)), - ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)), - ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings), - ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)), - ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)), - ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings), - selector, - minimumAge: formatInput(age - 2), - currentYear: formatInput(currentYear), - currentMonth: formatInput(currentMonth), - currentDay: formatInput(currentDay), - scope: formatInput(BigInt(scope)), - user_identifier: formatInput(BigInt(user_identifier)), - forbidden_countries_list: [...Array(120)].map((x) => '0'), + photoEOI: photoEOI, }; return { @@ -440,136 +558,38 @@ export function prepareAadhaarDiscloseTestData( }; } -export function prepareAadhaarDiscloseData( - qrData: string, - identityTree: LeanIMT, - nameAndDob_smt: SMT, - nameAndYob_smt: SMT, - scope: string, - secret: string, - user_identifier: string, - discloseAttributes: { - dateOfBirth?: boolean; - name?: boolean; - gender?: boolean; - idNumber?: boolean; - issuingState?: boolean; - minimumAge?: number; - forbiddenCountriesListPacked?: string[]; - ofac?: boolean; - } -) { - const sharedData = processQRDataSimple(qrData); - - const { currentYear, currentMonth, currentDay } = calculateAge( - sharedData.extractedFields.dob, - sharedData.extractedFields.mob, - sharedData.extractedFields.yob - ); - - const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0]; - const nullifier = nullifierHash(sharedData.extractedFields); - const packedCommitment = computePackedCommitment(sharedData.extractedFields); - const commitment = computeCommitment( - BigInt(secret), - BigInt(sharedData.qrHash), - nullifier, - packedCommitment, - BigInt(sharedData.photoHash) - ); - - const paddedName = computePaddedName(sharedData.extractedFields.name); - - const index = findIndexInTree(identityTree, BigInt(commitment)); - const { - siblings, - path: merkle_path, - leaf_depth, - } = generateMerkleProof(identityTree, index, COMMITMENT_TREE_DEPTH); - - const namedob_leaf = getNameDobLeafAadhaar( - sharedData.extractedFields.name, - sharedData.extractedFields.yob, - sharedData.extractedFields.mob, - sharedData.extractedFields.dob - ); - const nameyob_leaf = getNameYobLeafAahaar( - sharedData.extractedFields.name, - sharedData.extractedFields.yob - ); - - const { - root: ofac_name_dob_smt_root, - closestleaf: ofac_name_dob_smt_leaf_key, - siblings: ofac_name_dob_smt_siblings, - } = generateSMTProof(nameAndDob_smt, namedob_leaf); - - const { - root: ofac_name_yob_smt_root, - closestleaf: ofac_name_yob_smt_leaf_key, - siblings: ofac_name_yob_smt_siblings, - } = generateSMTProof(nameAndYob_smt, nameyob_leaf); +export function processQRData( + privKeyPem: string, + name?: string, + dateOfBirth?: string, + gender?: string, + pincode?: string, + state?: string, + timestamp?: string +): SharedQRData { + const finalName = name ?? 'Sumit Kumar'; + const finalDateOfBirth = dateOfBirth ?? '01-01-1984'; + const finalGender = gender ?? 'M'; + const finalPincode = pincode ?? '110051'; + const finalState = state ?? 'Delhi'; - const selectorArr: AadhaarField[] = []; - if (discloseAttributes.dateOfBirth) { - selectorArr.push('YEAR_OF_BIRTH'); - selectorArr.push('MONTH_OF_BIRTH'); - selectorArr.push('DAY_OF_BIRTH'); - } - if (discloseAttributes.name) { - selectorArr.push('NAME'); - } - if (discloseAttributes.gender) { - selectorArr.push('GENDER'); - } - if (discloseAttributes.idNumber) { - selectorArr.push('AADHAAR_LAST_4_DIGITS'); - } - if (discloseAttributes.issuingState) { - selectorArr.push('STATE'); - } - if (discloseAttributes.ofac) { - selectorArr.push('OFAC_NAME_DOB_CHECK'); - selectorArr.push('OFAC_NAME_YOB_CHECK'); + let QRData: string; + if (name || dateOfBirth || gender || pincode || state) { + const newTestData = generateTestData({ + privKeyPem, + data: testCustomData, + name: finalName, + dob: finalDateOfBirth, + gender: finalGender, + pincode: finalPincode, + state: finalState, + timestamp: timestamp, + }); + QRData = newTestData.testQRData; + } else { + QRData = testQRData.testQRData; + // console.log('testQRData:', testQRData); } - const selector = createSelector(selectorArr); - - const inputs = { - attestation_id: '3', - secret, - qrDataHash: BigInt(sharedData.qrHash).toString(), - gender: genderAscii.toString(), - yob: stringToAsciiArray(sharedData.extractedFields.yob), - mob: stringToAsciiArray(sharedData.extractedFields.mob), - dob: stringToAsciiArray(sharedData.extractedFields.dob), - name: formatInput(paddedName), - aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits), - pincode: stringToAsciiArray(sharedData.extractedFields.pincode), - state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')), - ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits), - photoHash: formatInput(BigInt(sharedData.photoHash)), - merkle_root: formatInput(BigInt(identityTree.root)), - leaf_depth: formatInput(leaf_depth), - path: formatInput(merkle_path), - siblings: formatInput(siblings), - ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)), - ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)), - ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings), - ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)), - ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)), - ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings), - selector, - minimumAge: formatInput(discloseAttributes.minimumAge ?? 0), - currentYear: formatInput(currentYear), - currentMonth: formatInput(currentMonth), - currentDay: formatInput(currentDay), - scope: formatInput(BigInt(scope)), - user_identifier: formatInput(BigInt(user_identifier)), - forbidden_countries_list: discloseAttributes.forbiddenCountriesListPacked - ? formatInput(formatCountriesList(discloseAttributes.forbiddenCountriesListPacked)) - : formatInput([...Array(120)].map((_) => '0')), - }; - - return inputs; + return processQRDataSimple(QRData); } diff --git a/common/src/utils/aadhaar/utils.ts b/common/src/utils/aadhaar/utils.ts index 772a450ac..e85a864c3 100644 --- a/common/src/utils/aadhaar/utils.ts +++ b/common/src/utils/aadhaar/utils.ts @@ -1,5 +1,8 @@ +import { ethers } from 'ethers'; import forge from 'node-forge'; +import { IDENTITY_VERIFICATION_HUB_ADDRESS, RPC_URL } from '../../constants/constants.js'; + import { convertBigIntToByteArray, decompressByteArray, @@ -94,6 +97,7 @@ export const createCustomV2TestData = ({ photo, name, timestamp, + aadhaarLast4Digits, }: { signedData: Uint8Array; dob?: string; @@ -103,6 +107,7 @@ export const createCustomV2TestData = ({ photo?: boolean; name?: string; timestamp?: string; + aadhaarLast4Digits?: string; }) => { const allDataParsed: number[][] = []; const delimiterIndices: number[] = []; @@ -123,6 +128,18 @@ export const createCustomV2TestData = ({ } } + console.log('createCustomV2TestData', { + signedData, + dob, + pincode, + gender, + state, + photo, + name, + timestamp, + aadhaarLast4Digits, + }); + // Set new timestamp to the time of the signature const newDateString = returnNewDateString(timestamp); const newTimestamp = new TextEncoder().encode(newDateString); @@ -175,6 +192,12 @@ export const createCustomV2TestData = ({ ); } + if (!aadhaarLast4Digits) { + for (let i = 2; i < 6; i++) { + modifiedSignedData[i] = Math.floor(Math.random() * 10) + 48; + } + } + if (name) { const newName = new TextEncoder().encode(name); modifiedSignedData = replaceBytesBetween( @@ -275,31 +298,16 @@ export function extractQRDataFields(qrData: string | Uint8Array): ExtractedQRDat const phoneData = extractFieldData(signedData, delimiterIndices, FIELD_POSITIONS.PHONE_NO); const phoneNoLast4Digits = asciiArrayToString(phoneData.slice(phoneData.length - 4)); - // Extract timestamp (from position after first delimiter) - // Timestamp format: YYYYMMDDHHMM (similar to circom implementation) - const timestampStartIndex = delimiterIndices[0] + 1; const timestampYear = asciiArrayToString([ - signedData[timestampStartIndex + 8], - signedData[timestampStartIndex + 9], - signedData[timestampStartIndex + 10], - signedData[timestampStartIndex + 11], - ]); - const timestampMonth = asciiArrayToString([ - signedData[timestampStartIndex + 12], - signedData[timestampStartIndex + 13], - ]); - const timestampDay = asciiArrayToString([ - signedData[timestampStartIndex + 14], - signedData[timestampStartIndex + 15], - ]); - const timestampHour = asciiArrayToString([ - signedData[timestampStartIndex + 16], - signedData[timestampStartIndex + 17], - ]); - const timestampMinute = asciiArrayToString([ - signedData[timestampStartIndex + 18], - signedData[timestampStartIndex + 19], + signedData[9], + signedData[10], + signedData[11], + signedData[12], ]); + const timestampMonth = asciiArrayToString([signedData[13], signedData[14]]); + const timestampDay = asciiArrayToString([signedData[15], signedData[16]]); + const timestampHour = asciiArrayToString([signedData[17], signedData[18]]); + const timestampMinute = asciiArrayToString([signedData[19], signedData[20]]); const timestamp = `${timestampYear}-${timestampMonth}-${timestampDay} ${timestampHour}:${timestampMinute}`; @@ -371,6 +379,24 @@ export const generateTestData = ({ return newQrData; }; +export async function getAadharRegistrationWindow() { + try { + const provider = new ethers.JsonRpcProvider(RPC_URL); + + const identityVerificationHub = new ethers.Contract( + IDENTITY_VERIFICATION_HUB_ADDRESS, + ['function AADHAAR_REGISTRATION_WINDOW() view returns (uint256)'], + provider + ); + + const aadharRegistrationWindow = await identityVerificationHub.AADHAAR_REGISTRATION_WINDOW(); + return aadharRegistrationWindow; + } catch (error) { + console.warn('Failed to get aadhar registration window:', error); + return 120; + } +} + export function returnNewDateString(timestamp?: string): string { const newDate = timestamp ? new Date(+timestamp) : new Date(); diff --git a/common/src/utils/circuits/circuitsName.ts b/common/src/utils/circuits/circuitsName.ts index 0da8cdeb0..bb83c3925 100644 --- a/common/src/utils/circuits/circuitsName.ts +++ b/common/src/utils/circuits/circuitsName.ts @@ -1,7 +1,7 @@ -import type { PassportData } from '../types.js'; +import type { IDDocument, PassportData } from '../types.js'; export function getCircuitNameFromPassportData( - passportData: PassportData, + passportData: IDDocument, circuitType: 'register' | 'dsc' ) { if (circuitType === 'register') { @@ -11,9 +11,13 @@ export function getCircuitNameFromPassportData( } } -function getDSCircuitNameFromPassportData(passportData: PassportData) { +function getDSCircuitNameFromPassportData(passportData: IDDocument) { console.log('Getting DSC circuit name from passport data...'); + if (passportData.documentCategory === 'aadhaar') { + throw new Error('Aadhaar does not have a DSC circuit'); + } + if (!passportData.passportMetadata) { console.error('Passport metadata is missing'); throw new Error('Passport data are not parsed'); @@ -76,9 +80,13 @@ function getDSCircuitNameFromPassportData(passportData: PassportData) { } } -function getRegisterNameFromPassportData(passportData: PassportData) { +function getRegisterNameFromPassportData(passportData: IDDocument) { console.log('Getting register circuit name from passport data...'); + if (passportData.documentCategory === 'aadhaar') { + return 'register_aadhaar'; + } + if (!passportData.passportMetadata) { console.error('Passport metadata is missing'); throw new Error('Passport data are not parsed'); diff --git a/common/src/utils/circuits/registerInputs.ts b/common/src/utils/circuits/registerInputs.ts index 2a219952c..12a579685 100644 --- a/common/src/utils/circuits/registerInputs.ts +++ b/common/src/utils/circuits/registerInputs.ts @@ -1,6 +1,7 @@ import { poseidon2 } from 'poseidon-lite'; import { + AADHAAR_ATTESTATION_ID, attributeToPosition, attributeToPosition_ID, DEFAULT_MAJORITY, @@ -17,13 +18,94 @@ import { getCircuitNameFromPassportData, hashEndpointWithScope, } from '../../utils/index.js'; -import type { OfacTree } from '../../utils/types.js'; +import type { AadhaarData, IDDocument, OfacTree } from '../../utils/types.js'; import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; import { SMT } from '@openpassport/zk-kit-smt'; export { generateCircuitInputsRegister } from './generateInputs.js'; +export function generateTEEInputsAadhaarDisclose( + secret: string, + aadhaarData: AadhaarData, + selfApp: SelfApp, + getTree: ( + doc: DocumentCategory, + tree: T + ) => T extends 'ofac' ? OfacTree : any +) { + const { prepareAadhaarDiscloseData } = require('../aadhaar/mockData.js'); + const { scope, disclosures, endpoint, userId, userDefinedData, chainID } = selfApp; + const userIdentifierHash = calculateUserIdentifierHash(chainID, userId, userDefinedData); + const scope_hash = hashEndpointWithScope(endpoint, scope); + + const ofac_trees = getTree('aadhaar', 'ofac'); + if (!ofac_trees) { + throw new Error('OFAC trees not loaded'); + } + + if (!ofac_trees.nameAndDob || !ofac_trees.nameAndYob) { + throw new Error('Invalid OFAC tree structure: missing required fields'); + } + + const nameAndDobSMT = new SMT(poseidon2, true); + const nameAndYobSMT = new SMT(poseidon2, true); + nameAndDobSMT.import(ofac_trees.nameAndDob); + nameAndYobSMT.import(ofac_trees.nameAndYob); + + const serialized_tree = getTree('aadhaar', 'commitment'); + const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serialized_tree); + + const inputs = prepareAadhaarDiscloseData( + aadhaarData.qrData, + tree, + nameAndDobSMT, + nameAndYobSMT, + scope_hash, + secret, + userIdentifierHash.toString(), + { + dateOfBirth: disclosures.date_of_birth, + name: disclosures.name, + gender: disclosures.gender, + idNumber: disclosures.passport_number, + issuingState: disclosures.issuing_state, + minimumAge: disclosures.minimumAge, + forbiddenCountriesListPacked: disclosures.excludedCountries, + ofac: disclosures.ofac, + } + ); + + return { + inputs, + circuitName: 'vc_and_disclose_aadhaar', + endpointType: selfApp.endpointType, + endpoint: selfApp.endpoint, + }; +} + +export async function generateTEEInputsAadhaarRegister( + secret: string, + aadhaarData: AadhaarData, + publicKeys: string[], + env: 'prod' | 'stg' +) { + const { prepareAadhaarRegisterData } = require('../aadhaar/mockData.js'); + console.log( + 'publicKeys-aadhaar', + publicKeys, + 'secret-aadhaar', + secret, + 'aadhaarData-aadhaar', + aadhaarData + ); + const inputs = await prepareAadhaarRegisterData(aadhaarData.qrData, secret, publicKeys); + const circuitName = 'register_aadhaar'; + const endpointType = env === 'stg' ? 'staging_celo' : 'celo'; + const endpoint = 'https://self.xyz'; + return { inputs, circuitName, endpointType, endpoint }; +} + export function generateTEEInputsDSC( passportData: PassportData, cscaTree: string[][], @@ -36,15 +118,63 @@ export function generateTEEInputsDSC( return { inputs, circuitName, endpointType, endpoint }; } +/*** DISCLOSURE ***/ + +function getSelectorDg1(document: DocumentCategory, disclosures: SelfAppDisclosureConfig) { + switch (document) { + case 'passport': + return getSelectorDg1Passport(disclosures); + case 'id_card': + return getSelectorDg1IdCard(disclosures); + } +} + +function getSelectorDg1Passport(disclosures: SelfAppDisclosureConfig) { + const selector_dg1 = Array(88).fill('0'); + Object.entries(disclosures).forEach(([attribute, reveal]) => { + if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) { + return; + } + if (reveal) { + const [start, end] = attributeToPosition[attribute as keyof typeof attributeToPosition]; + selector_dg1.fill('1', start, end + 1); + } + }); + return selector_dg1; +} + +function getSelectorDg1IdCard(disclosures: SelfAppDisclosureConfig) { + const selector_dg1 = Array(90).fill('0'); + Object.entries(disclosures).forEach(([attribute, reveal]) => { + if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) { + return; + } + if (reveal) { + const [start, end] = attributeToPosition_ID[attribute as keyof typeof attributeToPosition_ID]; + selector_dg1.fill('1', start, end + 1); + } + }); + return selector_dg1; +} + export function generateTEEInputsDiscloseStateless( secret: string, - passportData: PassportData, + passportData: IDDocument, selfApp: SelfApp, getTree: ( doc: DocumentCategory, tree: T ) => T extends 'ofac' ? OfacTree : any ) { + if (passportData.documentCategory === 'aadhaar') { + const { inputs, circuitName, endpointType, endpoint } = generateTEEInputsAadhaarDisclose( + secret, + passportData, + selfApp, + getTree + ); + return { inputs, circuitName, endpointType, endpoint }; + } const { scope, disclosures, endpoint, userId, userDefinedData, chainID } = selfApp; const userIdentifierHash = calculateUserIdentifierHash(chainID, userId, userDefinedData); const scope_hash = hashEndpointWithScope(endpoint, scope); @@ -107,54 +237,29 @@ export function generateTEEInputsDiscloseStateless( }; } -export function generateTEEInputsRegister( +export async function generateTEEInputsRegister( secret: string, - passportData: PassportData, - dscTree: string, + passportData: IDDocument, + dscTree: string | string[], env: 'prod' | 'stg' ) { - const inputs = generateCircuitInputsRegister(secret, passportData, dscTree); + if (passportData.documentCategory === 'aadhaar') { + const { inputs, circuitName, endpointType, endpoint } = await generateTEEInputsAadhaarRegister( + secret, + passportData, + dscTree as string[], + env + ); + console.log('inputs-aadhaar', inputs); + console.log('circuitName-aadhaar', circuitName); + console.log('endpointType-aadhaar', endpointType); + console.log('endpoint-aadhaar', endpoint); + return { inputs, circuitName, endpointType, endpoint }; + } + + const inputs = generateCircuitInputsRegister(secret, passportData, dscTree as string); const circuitName = getCircuitNameFromPassportData(passportData, 'register'); const endpointType = env === 'stg' ? 'staging_celo' : 'celo'; const endpoint = 'https://self.xyz'; return { inputs, circuitName, endpointType, endpoint }; } - -/*** DISCLOSURE ***/ - -function getSelectorDg1(document: DocumentCategory, disclosures: SelfAppDisclosureConfig) { - switch (document) { - case 'passport': - return getSelectorDg1Passport(disclosures); - case 'id_card': - return getSelectorDg1IdCard(disclosures); - } -} - -function getSelectorDg1Passport(disclosures: SelfAppDisclosureConfig) { - const selector_dg1 = Array(88).fill('0'); - Object.entries(disclosures).forEach(([attribute, reveal]) => { - if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) { - return; - } - if (reveal) { - const [start, end] = attributeToPosition[attribute as keyof typeof attributeToPosition]; - selector_dg1.fill('1', start, end + 1); - } - }); - return selector_dg1; -} - -function getSelectorDg1IdCard(disclosures: SelfAppDisclosureConfig) { - const selector_dg1 = Array(90).fill('0'); - Object.entries(disclosures).forEach(([attribute, reveal]) => { - if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) { - return; - } - if (reveal) { - const [start, end] = attributeToPosition_ID[attribute as keyof typeof attributeToPosition_ID]; - selector_dg1.fill('1', start, end + 1); - } - }); - return selector_dg1; -} diff --git a/common/src/utils/index.ts b/common/src/utils/index.ts index d408abb1f..f152e13c7 100644 --- a/common/src/utils/index.ts +++ b/common/src/utils/index.ts @@ -1,9 +1,9 @@ +export type { AadhaarData, DocumentCategory, PassportData } from './types.js'; export type { CertificateData, PublicKeyDetailsECDSA, PublicKeyDetailsRSA, } from './certificate_parsing/dataStructure.js'; -export type { DocumentCategory, PassportData } from './types.js'; export type { IdDocInput } from './passports/genMockIdDoc.js'; export type { PassportMetadata } from './passports/passport_parsing/parsePassportData.js'; export type { TEEPayload, TEEPayloadBase, TEEPayloadDisclose } from './proving.js'; @@ -19,6 +19,15 @@ export { export { bigIntToString, formatEndpoint, hashEndpointWithScope, stringToBigInt } from './scope.js'; export { brutforceSignatureAlgorithmDsc } from './passports/passport_parsing/brutForceDscSignature.js'; export { buildSMT, getLeafCscaTree, getLeafDscTree } from './trees.js'; +export { + calculateContentHash, + findStartPubKeyIndex, + generateCommitment, + generateNullifier, + inferDocumentCategory, + initPassportDataParsing, +} from './passports/passport.js'; +export { isAadhaarDocument, isMRZDocument } from './types.js'; export { calculateUserIdentifierHash, customHasher, @@ -36,14 +45,7 @@ export { getPayload, getWSDbRelayerUrl, } from './proving.js'; -export { - findStartPubKeyIndex, - generateCommitment, - generateNullifier, - initPassportDataParsing, - calculateContentHash, - inferDocumentCategory, -} from './passports/passport.js'; +export { extractQRDataFields, getAadharRegistrationWindow } from './aadhaar/utils.js'; export { formatMrz } from './passports/format.js'; export { genAndInitMockPassportData } from './passports/genMockPassportData.js'; export { @@ -57,6 +59,10 @@ export { generateCircuitInputsRegisterForTests, generateCircuitInputsVCandDisclose, } from './circuits/generateInputs.js'; +export { + generateTEEInputsAadhaarDisclose, + generateTEEInputsAadhaarRegister, +} from './circuits/registerInputs.js'; export { getCircuitNameFromPassportData } from './circuits/circuitsName.js'; export { getSKIPEM } from './csca.js'; export { initElliptic } from './certificate_parsing/elliptic.js'; diff --git a/common/src/utils/passports/genMockIdDoc.ts b/common/src/utils/passports/genMockIdDoc.ts index 3e3bcffa2..0717da4a2 100644 --- a/common/src/utils/passports/genMockIdDoc.ts +++ b/common/src/utils/passports/genMockIdDoc.ts @@ -7,6 +7,8 @@ import forge from 'node-forge'; import type { hashAlgosTypes } from '../../constants/constants.js'; import { API_URL_STAGING } from '../../constants/constants.js'; import { countries } from '../../constants/countries.js'; +import { convertByteArrayToBigInt, processQRData } from '../aadhaar/mockData.js'; +import { extractQRDataFields } from '../aadhaar/utils.js'; import { getCurveForElliptic } from '../certificate_parsing/curves.js'; import type { PublicKeyDetailsECDSA, @@ -14,14 +16,18 @@ import type { } from '../certificate_parsing/dataStructure.js'; import { parseCertificateSimple } from '../certificate_parsing/parseCertificateSimple.js'; import { getHashLen, hash } from '../hash.js'; -import type { DocumentType, PassportData, SignatureAlgorithm } from '../types.js'; +import type { AadhaarData, DocumentType, PassportData, SignatureAlgorithm } from '../types.js'; import { genDG1 } from './dg1.js'; import { formatAndConcatenateDataHashes, formatMrz, generateSignedAttr } from './format.js'; import { getMockDSC } from './getMockDSC.js'; import { initPassportDataParsing } from './passport.js'; +import { + AADHAAR_MOCK_PRIVATE_KEY_PEM, + AADHAAR_MOCK_PUBLIC_KEY_PEM, +} from '../../mock_certificates/aadhaar/mockAadhaarCert.js'; export interface IdDocInput { - idType: 'mock_passport' | 'mock_id_card'; + idType: 'mock_passport' | 'mock_id_card' | 'mock_aadhaar'; dgHashAlgo?: hashAlgosTypes; eContentHashAlgo?: hashAlgosTypes; signatureType?: SignatureAlgorithm; @@ -32,6 +38,9 @@ export interface IdDocInput { lastName?: string; firstName?: string; sex?: 'M' | 'F'; + // Aadhaar-specific fields + pincode?: string; // - not disclosing this so not getting it in CreateMockScreen + state?: string; } const defaultIdDocInput: IdDocInput = { @@ -43,19 +52,79 @@ const defaultIdDocInput: IdDocInput = { birthDate: '900101', expiryDate: '300101', passportNumber: '123456789', - lastName: 'DOE', - firstName: 'JOHN', + lastName: undefined, + firstName: undefined, sex: 'M', + // Aadhaar defaults + pincode: '110051', + state: 'Delhi', }; +// Generate mock Aadhaar document +function genMockAadhaarDoc(input: IdDocInput): AadhaarData { + const name = input.firstName + ? `${input.firstName} ${input.lastName || ''}`.trim() + : generateRandomName(); + + const gender = input.sex === 'F' ? 'F' : 'M'; + const pincode = input.pincode ?? '110051'; + const state = input.state ?? 'Delhi'; + const dateOfBirth = input.birthDate ?? '01-01-1990'; + console.log('genMockAadhaarDoc', input); + console.log('dateOfBirth', dateOfBirth); + + // Generate Aadhaar QR data using processQRData + const qrData = processQRData( + AADHAAR_MOCK_PRIVATE_KEY_PEM, + name, + dateOfBirth, + gender, + pincode, + state, + new Date().getTime().toString() + ); + + // Convert QR data to string format + const qrDataString = convertByteArrayToBigInt(qrData.qrDataBytes).toString(); + console.log('qrDataString', qrDataString); + + // Extract signature from the decoded data + const signatureBytes = qrData.decodedData.slice( + qrData.decodedData.length - 256, + qrData.decodedData.length + ); + const signature = Array.from(signatureBytes); + + console.log('qrData.extractedFields', qrData.extractedFields); + + return { + documentType: input.idType as DocumentType, + documentCategory: 'aadhaar', + mock: true, + qrData: qrDataString, + extractedFields: qrData.extractedFields, + signature, + publicKey: AADHAAR_MOCK_PUBLIC_KEY_PEM, + photoHash: qrData.photoHash.toString(), + }; +} + export function genMockIdDoc( userInput: Partial = {}, mockDSC?: { dsc: string; privateKeyPem: string } -): PassportData { +): PassportData | AadhaarData { + if (userInput.idType === 'mock_aadhaar') { + return genMockAadhaarDoc(userInput as IdDocInput); + } + const mergedInput: IdDocInput = { ...defaultIdDocInput, ...userInput, }; + + mergedInput.lastName = mergedInput.lastName ?? 'DOE'; + mergedInput.firstName = mergedInput.firstName ?? 'JOHN'; + let privateKeyPem: string, dsc: string; if (mockDSC) { dsc = mockDSC.dsc; @@ -91,7 +160,7 @@ export function genMockIdDoc( export function genMockIdDocAndInitDataParsing(userInput: Partial = {}) { return initPassportDataParsing({ - ...genMockIdDoc(userInput), + ...(genMockIdDoc(userInput) as PassportData), }); } @@ -116,6 +185,21 @@ export async function generateMockDSC( return { privateKeyPem: data.data.privateKeyPem, dsc: data.data.dsc }; } +function generateRandomName(): string { + // Generate random letter combinations for first and last name + const generateRandomLetters = (length: number): string => { + const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + return Array.from({ length }, () => letters[Math.floor(Math.random() * letters.length)]).join( + '' + ); + }; + + const firstName = generateRandomLetters(4 + Math.floor(Math.random() * 4)); // 4-7 letters + const lastName = generateRandomLetters(5 + Math.floor(Math.random() * 5)); // 5-9 letters + + return `${firstName} ${lastName}`; +} + function generateRandomBytes(length: number): number[] { // Generate numbers between -128 and 127 to match the existing signed byte format return Array.from({ length }, () => Math.floor(Math.random() * 256) - 128); diff --git a/common/src/utils/passports/passport.ts b/common/src/utils/passports/passport.ts index 15da0fb1a..d4daf0a51 100644 --- a/common/src/utils/passports/passport.ts +++ b/common/src/utils/passports/passport.ts @@ -1,3 +1,4 @@ +import { sha256 } from 'js-sha256'; import forge from 'node-forge'; import { poseidon5 } from 'poseidon-lite'; @@ -14,6 +15,7 @@ import { n_dsc_4096, n_dsc_ecdsa, } from '../../constants/constants.js'; +import { nullifierHash } from '../aadhaar/mockData.js'; import { bytesToBigDecimal, hexToDecimal, splitToWords } from '../bytes.js'; import type { CertificateData, @@ -29,10 +31,30 @@ import { findStartIndex, findStartIndexEC } from '../csca.js'; import { hash, packBytesAndPoseidon } from '../hash.js'; import { sha384_512Pad, shaPad } from '../shaPad.js'; import { getLeafDscTree } from '../trees.js'; -import type { DocumentCategory, PassportData, SignatureAlgorithm } from '../types.js'; +import type { DocumentCategory, IDDocument, PassportData, SignatureAlgorithm } from '../types.js'; +import { AadhaarData, isAadhaarDocument, isMRZDocument } from '../types.js'; import { formatMrz } from './format.js'; import { parsePassportData } from './passport_parsing/parsePassportData.js'; -import { sha256 } from 'js-sha256'; + +export function calculateContentHash(passportData: PassportData | AadhaarData): string { + if (isMRZDocument(passportData) && passportData.eContent) { + // eContent is likely a buffer or array, convert to string properly + const eContentStr = + typeof passportData.eContent === 'string' + ? passportData.eContent + : JSON.stringify(passportData.eContent); + + return sha256(eContentStr); + } + + // For MRZ documents without eContent, hash core stable fields + const stableData = { + documentType: passportData.documentType, + data: isMRZDocument(passportData) ? passportData.mrz : passportData.qrData || '', + documentCategory: passportData.documentCategory, + }; + return sha256(JSON.stringify(stableData)); +} export function extractRSFromSignature(signatureBytes: number[]): { r: string; s: string } { const derSignature = Buffer.from(signatureBytes).toString('binary'); @@ -152,6 +174,10 @@ export function generateCommitment( } function getPassportSignature(passportData: PassportData, n: number, k: number): any { + // if (isAadhaarDocument(passportData)) { + // return splitToWords(BigInt(bytesToBigDecimal(passportData.signature)), n, k); + // } + const { signatureAlgorithm } = passportData.dsc_parsed; if (signatureAlgorithm === 'ecdsa') { const { r, s } = extractRSFromSignature(passportData.encryptedDigest); @@ -163,7 +189,11 @@ function getPassportSignature(passportData: PassportData, n: number, k: number): } } -export function generateNullifier(passportData: PassportData) { +export function generateNullifier(passportData: IDDocument) { + if (isAadhaarDocument(passportData)) { + return nullifierHash(passportData.extractedFields); + } + const signedAttr_shaBytes = hash( passportData.passportMetadata.signedAttrHashFunction, Array.from(passportData.signedAttr), @@ -285,6 +315,17 @@ export function getSignatureAlgorithmFullName( } } +export function inferDocumentCategory(documentType: string): DocumentCategory { + if (documentType.includes('passport')) { + return 'passport' as DocumentCategory; + } else if (documentType.includes('id')) { + return 'id_card' as DocumentCategory; + } else if (documentType.includes('aadhaar')) { + return 'aadhaar' as DocumentCategory; + } + return 'passport' as DocumentCategory; // fallback +} + /// @dev will bruteforce passport and dsc signature export function initPassportDataParsing(passportData: PassportData, skiPem: any = null) { const passportMetadata = parsePassportData(passportData, skiPem); @@ -307,34 +348,3 @@ export function pad(hashFunction: (typeof hashAlgos)[number]) { export function padWithZeroes(bytes: number[], length: number) { return bytes.concat(new Array(length - bytes.length).fill(0)); } - -export function calculateContentHash(passportData: PassportData): string { - if (passportData.eContent) { - // eContent is likely a buffer or array, convert to string properly - const eContentStr = - typeof passportData.eContent === 'string' - ? passportData.eContent - : JSON.stringify(passportData.eContent); - - return sha256(eContentStr); - } - // For documents without eContent (like aadhaar), hash core stable fields - const stableData = { - documentType: passportData.documentType, - data: passportData.mrz || '', // Use mrz for passports/IDs, could be other data for aadhaar - documentCategory: passportData.documentCategory, - }; - - return sha256(JSON.stringify(stableData)); -} - -export function inferDocumentCategory(documentType: string): DocumentCategory { - if (documentType.includes('passport')) { - return 'passport' as DocumentCategory; - } else if (documentType.includes('id')) { - return 'id_card' as DocumentCategory; - } else if (documentType.includes('aadhaar')) { - return 'aadhaar' as DocumentCategory; - } - return 'passport' as DocumentCategory; // fallback -} diff --git a/common/src/utils/passports/validate.ts b/common/src/utils/passports/validate.ts index 3e4bb6cab..a8cd43f11 100644 --- a/common/src/utils/passports/validate.ts +++ b/common/src/utils/passports/validate.ts @@ -17,9 +17,17 @@ import { hash } from '../../utils/hash/sha.js'; import { formatMrz } from '../../utils/passports/format.js'; import { getLeafDscTree } from '../../utils/trees.js'; import { + computeCommitment, + computePackedCommitment, + nullifierHash, + processQRDataSimple, +} from '../aadhaar/mockData.js'; +import { + AadhaarData, AttestationIdHex, type DeployedCircuits, type DocumentCategory, + IDDocument, type PassportData, } from '../types.js'; import { generateCommitment, generateNullifier } from './passport.js'; @@ -33,8 +41,37 @@ export type PassportSupportStatus = | 'dsc_circuit_not_supported' | 'passport_supported'; -export async function checkDocumentSupported( +function validateRegistrationCircuit( + passportData: IDDocument, + deployedCircuits: DeployedCircuits +): { isValid: boolean; circuitName: string | null } { + let circuitNameRegister = getCircuitNameFromPassportData( + passportData as PassportData, + 'register' + ); + + const isValid = + circuitNameRegister && + (deployedCircuits.REGISTER.includes(circuitNameRegister) || + deployedCircuits.REGISTER_ID.includes(circuitNameRegister) || + deployedCircuits.REGISTER_AADHAAR.includes(circuitNameRegister)); + return { isValid: !!isValid, circuitName: circuitNameRegister }; +} + +function validateDscCircuit( passportData: PassportData, + deployedCircuits: DeployedCircuits +): { isValid: boolean; circuitName: string | null } { + const circuitNameDsc = getCircuitNameFromPassportData(passportData, 'dsc'); + const isValid = + circuitNameDsc && + (deployedCircuits.DSC.includes(circuitNameDsc) || + deployedCircuits.DSC_ID.includes(circuitNameDsc)); + return { isValid: !!isValid, circuitName: circuitNameDsc }; +} + +export async function checkDocumentSupported( + passportData: IDDocument, opts: { getDeployedCircuits: (docCategory: DocumentCategory) => DeployedCircuits; } @@ -42,8 +79,20 @@ export async function checkDocumentSupported( status: PassportSupportStatus; details: string; }> { + const deployedCircuits = opts.getDeployedCircuits(passportData.documentCategory); + if (passportData.documentCategory === 'aadhaar') { + const { isValid, circuitName } = validateRegistrationCircuit(passportData, deployedCircuits); + + if (!isValid) { + return { + status: 'registration_circuit_not_supported', + details: circuitName, + }; + } + return { status: 'passport_supported', details: circuitName }; + } + const passportMetadata = passportData.passportMetadata; - const document: DocumentCategory = passportData.documentCategory; if (!passportMetadata) { console.warn('Passport metadata is null'); return { status: 'passport_metadata_missing', details: passportData.dsc }; @@ -52,36 +101,30 @@ export async function checkDocumentSupported( console.warn('CSCA not found'); return { status: 'csca_not_found', details: passportData.dsc }; } - const circuitNameRegister = getCircuitNameFromPassportData(passportData, 'register'); - const deployedCircuits = opts.getDeployedCircuits(passportData.documentCategory); - if ( - !circuitNameRegister || - !( - deployedCircuits.REGISTER.includes(circuitNameRegister) || - deployedCircuits.REGISTER_ID.includes(circuitNameRegister) - ) - ) { + + const { isValid: isRegisterValid, circuitName: registerCircuitName } = + validateRegistrationCircuit(passportData, deployedCircuits); + if (!isRegisterValid) { return { status: 'registration_circuit_not_supported', - details: circuitNameRegister, + details: registerCircuitName, }; } - const circuitNameDsc = getCircuitNameFromPassportData(passportData, 'dsc'); - if ( - !circuitNameDsc || - !( - deployedCircuits.DSC.includes(circuitNameDsc) || - deployedCircuits.DSC_ID.includes(circuitNameDsc) - ) - ) { - console.warn('DSC circuit not supported:', circuitNameDsc); - return { status: 'dsc_circuit_not_supported', details: circuitNameDsc }; + + const { isValid: isDscValid, circuitName: dscCircuitName } = validateDscCircuit( + passportData as PassportData, + deployedCircuits + ); + if (!isDscValid) { + console.warn('DSC circuit not supported:', dscCircuitName); + return { status: 'dsc_circuit_not_supported', details: dscCircuitName }; } - return { status: 'passport_supported', details: 'null' }; + + return { status: 'passport_supported', details: dscCircuitName }; } export async function checkIfPassportDscIsInTree( - passportData: PassportData, + passportData: IDDocument, dscTree: string ): Promise { const hashFunction = (a: bigint, b: bigint) => poseidon2([a, b]); @@ -146,13 +189,58 @@ export function generateCommitmentInApp( return { commitment_list, csca_list }; } -export async function isDocumentNullified(passportData: PassportData) { +export function generateCommitmentInAppAadhaar( + secret: string, + attestation_id: string, + passportData: AadhaarData, + alternativePublicKeys: Record +) { + const nullifier = nullifierHash(passportData.extractedFields); + const packedCommitment = computePackedCommitment(passportData.extractedFields); + const { qrHash, photoHash } = processQRDataSimple(passportData.qrData); + + const publicKey_list: string[] = []; + const commitment_list: string[] = []; + + // For Aadhaar, we can also use the document's own public key + const allPublicKeys = { + document_public_key: passportData.publicKey, + ...alternativePublicKeys, + }; + + for (const [keyName, publicKeyValue] of Object.entries(allPublicKeys)) { + try { + const commitment = computeCommitment( + BigInt(secret), + BigInt(qrHash), + nullifier, + packedCommitment, + photoHash + ).toString(); + + publicKey_list.push(publicKeyValue); + commitment_list.push(commitment); + } catch (error) { + console.warn(`Failed to process public key for ${keyName}:`, error); + } + } + + if (commitment_list.length === 0) { + console.error('No valid public keys found for Aadhaar'); + } + + return { commitment_list, publicKey_list }; +} + +export async function isDocumentNullified(passportData: IDDocument) { const nullifier = generateNullifier(passportData); const nullifierHex = `0x${BigInt(nullifier).toString(16)}`; const attestationId = passportData.documentCategory === 'passport' ? AttestationIdHex.passport - : AttestationIdHex.id_card; + : passportData.documentCategory === 'aadhaar' + ? AttestationIdHex.aadhaar + : AttestationIdHex.id_card; console.log('checking for nullifier', nullifierHex, attestationId); const baseUrl = passportData.mock === false ? API_URL : API_URL_STAGING; const controller = new AbortController(); @@ -181,17 +269,38 @@ export async function isDocumentNullified(passportData: PassportData) { } export async function isUserRegistered( - passportData: PassportData, + documentData: PassportData | AadhaarData, secret: string, getCommitmentTree: (docCategory: DocumentCategory) => string ) { - if (!passportData) { + if (!documentData) { return false; } - const attestationId = - passportData.documentCategory === 'passport' ? PASSPORT_ATTESTATION_ID : ID_CARD_ATTESTATION_ID; - const commitment = generateCommitment(secret, attestationId, passportData); - const document: DocumentCategory = passportData.documentCategory; + + const document: DocumentCategory = documentData.documentCategory; + let commitment: string; + + if (document === 'aadhaar') { + const aadhaarData = documentData as AadhaarData; + const nullifier = nullifierHash(aadhaarData.extractedFields); + const packedCommitment = computePackedCommitment(aadhaarData.extractedFields); + const { qrHash, photoHash } = processQRDataSimple(aadhaarData.qrData); + + commitment = computeCommitment( + BigInt(secret), + BigInt(qrHash), + nullifier, + packedCommitment, + photoHash + ).toString(); + + console.log('commitment', commitment); + } else { + const attestationId = + document === 'passport' ? PASSPORT_ATTESTATION_ID : ID_CARD_ATTESTATION_ID; + commitment = generateCommitment(secret, attestationId, documentData as PassportData); + } + const serializedTree = getCommitmentTree(document); const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serializedTree); const index = tree.indexOf(BigInt(commitment)); @@ -199,7 +308,7 @@ export async function isUserRegistered( } export async function isUserRegisteredWithAlternativeCSCA( - passportData: PassportData, + passportData: IDDocument, secret: string, { getCommitmentTree, @@ -214,21 +323,56 @@ export async function isUserRegisteredWithAlternativeCSCA( return { isRegistered: false, csca: null }; } const document: DocumentCategory = passportData.documentCategory; - const alternativeCSCA = getAltCSCA(document); - const { commitment_list, csca_list } = generateCommitmentInApp( - secret, - document === 'passport' ? PASSPORT_ATTESTATION_ID : ID_CARD_ATTESTATION_ID, - passportData, - alternativeCSCA - ); + let commitment_list: string[]; + let csca_list: string[]; + + if (document === 'aadhaar') { + // For Aadhaar, use public keys from protocol store instead of CSCA + const publicKeys = getAltCSCA(document); + if (!publicKeys || Object.keys(publicKeys).length === 0) { + console.error('No public keys available for Aadhaar'); + return { isRegistered: false, csca: null }; + } + + // Create alternative public keys object from protocol store + const alternativePublicKeys: Record = {}; + Object.entries(publicKeys).forEach(([key, value], index) => { + alternativePublicKeys[`public_key_${index}`] = value; + }); + + const result = generateCommitmentInAppAadhaar( + secret, + AttestationIdHex.aadhaar, + passportData as AadhaarData, + alternativePublicKeys + ); + commitment_list = result.commitment_list; + csca_list = result.publicKey_list; + } else { + // For passport/id_card, use CSCA certificates + const alternativeCSCA = getAltCSCA(document); + const result = generateCommitmentInApp( + secret, + document === 'passport' ? PASSPORT_ATTESTATION_ID : ID_CARD_ATTESTATION_ID, + passportData as PassportData, + alternativeCSCA + ); + commitment_list = result.commitment_list; + csca_list = result.csca_list; + } if (commitment_list.length === 0) { - console.error('No valid CSCA certificates could be parsed from alternativeCSCA'); + const errorMsg = + document === 'aadhaar' + ? 'No valid public keys could be processed for Aadhaar' + : 'No valid CSCA certificates could be parsed from alternativeCSCA'; + console.error(errorMsg); return { isRegistered: false, csca: null }; } const serializedTree = getCommitmentTree(document); const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serializedTree); + for (let i = 0; i < commitment_list.length; i++) { const commitment = commitment_list[i]; const index = tree.indexOf(BigInt(commitment)); @@ -236,7 +380,12 @@ export async function isUserRegisteredWithAlternativeCSCA( return { isRegistered: true, csca: csca_list[i] }; } } - console.warn('None of the following CSCA correspond to the commitment:', csca_list); + + const warnMsg = + document === 'aadhaar' + ? `None of the following public keys correspond to the commitment for Aadhaar: ${csca_list}` + : `None of the following CSCA correspond to the commitment: ${csca_list}`; + console.warn(warnMsg); return { isRegistered: false, csca: null }; } diff --git a/common/src/utils/proving.ts b/common/src/utils/proving.ts index eb6446c82..91d53db29 100644 --- a/common/src/utils/proving.ts +++ b/common/src/utils/proving.ts @@ -32,9 +32,9 @@ export const ec = new EC('p256'); // eslint-disable-next-line -- clientKey is created from ec so must be second export const clientKey = ec.genKeyPair(); -type RegisterSuffixes = '' | '_id'; +type RegisterSuffixes = '' | '_id' | '_aadhaar'; type DscSuffixes = '' | '_id'; -type DiscloseSuffixes = '' | '_id'; +type DiscloseSuffixes = '' | '_id' | '_aadhaar'; type ProofTypes = 'register' | 'dsc' | 'disclose'; type RegisterProofType = `${Extract}${RegisterSuffixes}`; type DscProofType = `${Extract}${DscSuffixes}`; @@ -67,8 +67,14 @@ export function getPayload( userDefinedData: string = '' ) { if (circuitType === 'disclose') { + const type = + circuitName === 'vc_and_disclose' + ? 'disclose' + : circuitName === 'vc_and_disclose_aadhaar' + ? 'disclose_aadhaar' + : 'disclose_id'; const payload: TEEPayloadDisclose = { - type: circuitName === 'vc_and_disclose' ? 'disclose' : 'disclose_id', + type, endpointType: endpointType, endpoint: endpoint, onchain: endpointType === 'celo' ? true : false, @@ -81,8 +87,9 @@ export function getPayload( }; return payload; } else { + const type = circuitName === 'register_aadhaar' ? 'register_aadhaar' : circuitType; const payload: TEEPayload = { - type: circuitType as RegisterProofType | DscProofType, + type: type as RegisterProofType | DscProofType, onchain: true, endpointType: endpointType, circuit: { diff --git a/common/src/utils/types.ts b/common/src/utils/types.ts index 7595df551..2d6e73559 100644 --- a/common/src/utils/types.ts +++ b/common/src/utils/types.ts @@ -1,9 +1,30 @@ +import type { ExtractedQRData } from './aadhaar/utils.js'; import type { CertificateData } from './certificate_parsing/dataStructure.js'; import type { PassportMetadata } from './passports/passport_parsing/parsePassportData.js'; +// Base interface for common fields +interface BaseIDData { + documentType: DocumentType; + documentCategory: DocumentCategory; + mock: boolean; + dsc_parsed?: CertificateData; + csca_parsed?: CertificateData; +} + +// Aadhaar document data +export interface AadhaarData extends BaseIDData { + documentCategory: 'aadhaar'; + qrData: string; + extractedFields: ExtractedQRData; // All parsed Aadhaar fields + signature: number[]; + publicKey: string; + photoHash?: string; +} + export type DeployedCircuits = { REGISTER: string[]; REGISTER_ID: string[]; + REGISTER_AADHAAR: string[]; DSC: string[]; DSC_ID: string[]; }; @@ -13,7 +34,7 @@ export interface DocumentCatalog { selectedDocumentId?: string; // This is now a contentHash } -export type DocumentCategory = 'passport' | 'id_card'; +export type DocumentCategory = 'passport' | 'id_card' | 'aadhaar'; export interface DocumentMetadata { id: string; // contentHash as ID for deduplication @@ -24,7 +45,15 @@ export interface DocumentMetadata { isRegistered?: boolean; // whether the document is registered onChain } -export type DocumentType = 'passport' | 'id_card' | 'mock_passport' | 'mock_id_card'; +export type DocumentType = + | 'passport' + | 'id_card' + | 'aadhaar' + | 'mock_passport' + | 'mock_id_card' + | 'mock_aadhaar'; + +export type IDDocument = AadhaarData | PassportData; export type OfacTree = { passportNoAndNationality: any; @@ -32,7 +61,9 @@ export type OfacTree = { nameAndYob: any; }; -export type PassportData = { +// Define the signature algorithm in "algorithm_hashfunction_domainPapameter_keyLength" +export interface PassportData extends BaseIDData { + documentCategory: 'passport' | 'id_card'; mrz: string; dg1Hash?: number[]; dg2Hash?: number[]; @@ -42,12 +73,7 @@ export type PassportData = { signedAttr: number[]; encryptedDigest: number[]; passportMetadata?: PassportMetadata; - dsc_parsed?: CertificateData; - csca_parsed?: CertificateData; - documentType: DocumentType; - documentCategory: DocumentCategory; - mock: boolean; -}; +} export type Proof = { proof: { @@ -119,6 +145,7 @@ export enum AttestationIdHex { invalid = '0x0000000000000000000000000000000000000000000000000000000000000000', passport = '0x0000000000000000000000000000000000000000000000000000000000000001', id_card = '0x0000000000000000000000000000000000000000000000000000000000000002', + aadhaar = '0x0000000000000000000000000000000000000000000000000000000000000003', } export function castCSCAProof(proof: any): Proof { @@ -131,3 +158,17 @@ export function castCSCAProof(proof: any): Proof { pub_signals: proof.pub_signals, }; } + +export function isAadhaarDocument( + passportData: PassportData | AadhaarData +): passportData is AadhaarData { + return passportData.documentCategory === 'aadhaar'; +} + +export function isMRZDocument( + passportData: PassportData | AadhaarData +): passportData is PassportData { + return ( + passportData.documentCategory === 'passport' || passportData.documentCategory === 'id_card' + ); +} diff --git a/common/tsup.config.ts b/common/tsup.config.ts index eb2e15877..8dc7b36be 100644 --- a/common/tsup.config.ts +++ b/common/tsup.config.ts @@ -20,6 +20,8 @@ const entry = { 'src/constants/sampleDataHashes': 'src/constants/sampleDataHashes.ts', // Granular utils exports 'src/utils/aadhaar/constants': 'src/utils/aadhaar/constants.ts', + 'src/utils/aadhaar/utils': 'src/utils/aadhaar/utils.ts', + 'src/utils/aadhaar/mockData': 'src/utils/aadhaar/mockData.ts', 'src/utils/attest': 'src/utils/attest.ts', 'src/utils/hash': 'src/utils/hash.ts', 'src/utils/bytes': 'src/utils/bytes.ts', diff --git a/contracts/error-selectors.json b/contracts/error-selectors.json index 9941e21de..d096d11a2 100644 --- a/contracts/error-selectors.json +++ b/contracts/error-selectors.json @@ -1,10 +1,17 @@ [ + { + "name": "REGISTERED_COMMITMENT", + "signature": "REGISTERED_COMMITMENT()", + "selector": "0x034acfcc", + "file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol", + "line": 134 + }, { "name": "REGISTERED_COMMITMENT", "signature": "REGISTERED_COMMITMENT()", "selector": "0x034acfcc", "file": "contracts/registry/IdentityRegistryIdCardImplV1.sol", - "line": 141 + "line": 142 }, { "name": "REGISTERED_COMMITMENT", @@ -18,42 +25,42 @@ "signature": "InvalidProof()", "selector": "0x09bde339", "file": "contracts/example/Airdrop.sol", - "line": 54 + "line": 57 }, { "name": "NoVerifierSet", "signature": "NoVerifierSet()", "selector": "0x0ee78d58", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 110 + "line": 136 }, { "name": "InvalidAttestationId", "signature": "InvalidAttestationId()", "selector": "0x12ec75fe", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 142 + "line": 168 }, { "name": "InvalidAttestationId", "signature": "InvalidAttestationId()", "selector": "0x12ec75fe", "file": "contracts/libraries/CustomVerifier.sol", - "line": 11 + "line": 10 }, { "name": "RegistrationNotOpen", "signature": "RegistrationNotOpen()", "selector": "0x153745d3", "file": "contracts/example/Airdrop.sol", - "line": 63 + "line": 66 }, { "name": "InvalidDscProof", "signature": "InvalidDscProof()", "selector": "0x1644e049", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 122 + "line": 148 }, { "name": "InvalidYearRange", @@ -76,12 +83,19 @@ "file": "contracts/IdentityVerificationHubImplV1.sol", "line": 166 }, + { + "name": "HUB_ADDRESS_ZERO", + "signature": "HUB_ADDRESS_ZERO()", + "selector": "0x22697ffa", + "file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol", + "line": 138 + }, { "name": "RegisteredNullifier", "signature": "RegisteredNullifier()", "selector": "0x22cbc6a2", "file": "contracts/example/Airdrop.sol", - "line": 78 + "line": 81 }, { "name": "InvalidMonthRange", @@ -95,7 +109,7 @@ "signature": "UserIdentifierAlreadyRegistered()", "selector": "0x29393238", "file": "contracts/example/Airdrop.sol", - "line": 75 + "line": 78 }, { "name": "InvalidFieldElement", @@ -104,26 +118,40 @@ "file": "contracts/libraries/Formatter.sol", "line": 13 }, + { + "name": "InvalidPubkey", + "signature": "InvalidPubkey()", + "selector": "0x422cc3b7", + "file": "contracts/IdentityVerificationHubImplV2.sol", + "line": 196 + }, { "name": "InvalidOlderThan", "signature": "InvalidOlderThan()", "selector": "0x49aecbc2", "file": "contracts/libraries/CustomVerifier.sol", - "line": 14 + "line": 13 }, { "name": "InvalidDscCommitmentRoot", "signature": "InvalidDscCommitmentRoot()", "selector": "0x4cb305bb", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 134 + "line": 160 + }, + { + "name": "HUB_NOT_SET", + "signature": "HUB_NOT_SET()", + "selector": "0x4ffa9998", + "file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol", + "line": 130 }, { "name": "HUB_NOT_SET", "signature": "HUB_NOT_SET()", "selector": "0x4ffa9998", "file": "contracts/registry/IdentityRegistryIdCardImplV1.sol", - "line": 137 + "line": 138 }, { "name": "HUB_NOT_SET", @@ -151,6 +179,13 @@ "signature": "UserIdentifierAlreadyMinted()", "selector": "0x5dd09265", "file": "contracts/example/SelfIdentityERC721.sol", + "line": 51 + }, + { + "name": "UserIdentifierAlreadyMinted", + "signature": "UserIdentifierAlreadyMinted()", + "selector": "0x5dd09265", + "file": "contracts/example/SelfPassportERC721.sol", "line": 48 }, { @@ -158,49 +193,49 @@ "signature": "InvalidOfacCheck()", "selector": "0x5fb542f4", "file": "contracts/libraries/CustomVerifier.sol", - "line": 12 + "line": 11 }, { "name": "CrossChainIsNotSupportedYet", "signature": "CrossChainIsNotSupportedYet()", "selector": "0x61296fbb", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 150 + "line": 176 }, { "name": "AlreadyClaimed", "signature": "AlreadyClaimed()", "selector": "0x646cf558", "file": "contracts/example/Airdrop.sol", - "line": 57 + "line": 60 }, { "name": "AlreadyClaimed", "signature": "AlreadyClaimed()", "selector": "0x646cf558", "file": "contracts/example/HappyBirthday.sol", - "line": 64 + "line": 67 }, { "name": "InputTooShort", "signature": "InputTooShort()", "selector": "0x65ec0cf1", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 154 + "line": 180 }, { "name": "InvalidRegisterProof", "signature": "InvalidRegisterProof()", "selector": "0x67b61dc7", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 118 + "line": 144 }, { "name": "RegistrationNotClosed", "signature": "RegistrationNotClosed()", "selector": "0x697e379b", "file": "contracts/example/Airdrop.sol", - "line": 66 + "line": 69 }, { "name": "INVALID_DSC_PROOF", @@ -214,7 +249,7 @@ "signature": "ClaimNotOpen()", "selector": "0x6b687806", "file": "contracts/example/Airdrop.sol", - "line": 69 + "line": 72 }, { "name": "INVALID_OFAC", @@ -223,12 +258,19 @@ "file": "contracts/IdentityVerificationHubImplV1.sol", "line": 146 }, + { + "name": "InvalidUidaiTimestamp", + "signature": "InvalidUidaiTimestamp()", + "selector": "0x72b3dac6", + "file": "contracts/IdentityVerificationHubImplV2.sol", + "line": 200 + }, { "name": "InvalidForbiddenCountries", "signature": "InvalidForbiddenCountries()", "selector": "0x82cba848", "file": "contracts/libraries/CustomVerifier.sol", - "line": 13 + "line": 12 }, { "name": "InsufficientCharcodeLen", @@ -242,7 +284,7 @@ "signature": "InsufficientCharcodeLen()", "selector": "0x86d41225", "file": "contracts/libraries/CircuitAttributeHandlerV2.sol", - "line": 17 + "line": 16 }, { "name": "InsufficientCharcodeLen", @@ -277,7 +319,7 @@ "signature": "InvalidCscaRoot()", "selector": "0x8f1b44c7", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 138 + "line": 164 }, { "name": "INVALID_REGISTER_PROOF", @@ -291,14 +333,14 @@ "signature": "UserContextDataTooShort()", "selector": "0x94ec3503", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 158 + "line": 184 }, { "name": "NotWithinBirthdayWindow", "signature": "NotWithinBirthdayWindow()", "selector": "0x9b7983d7", "file": "contracts/example/HappyBirthday.sol", - "line": 63 + "line": 66 }, { "name": "INVALID_CSCA_ROOT", @@ -319,7 +361,7 @@ "signature": "ConfigNotSet()", "selector": "0xace124bc", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 166 + "line": 192 }, { "name": "InvalidDateLength", @@ -328,12 +370,19 @@ "file": "contracts/libraries/Formatter.sol", "line": 9 }, + { + "name": "ONLY_HUB_CAN_ACCESS", + "signature": "ONLY_HUB_CAN_ACCESS()", + "selector": "0xba0318cb", + "file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol", + "line": 132 + }, { "name": "ONLY_HUB_CAN_ACCESS", "signature": "ONLY_HUB_CAN_ACCESS()", "selector": "0xba0318cb", "file": "contracts/registry/IdentityRegistryIdCardImplV1.sol", - "line": 139 + "line": 140 }, { "name": "ONLY_HUB_CAN_ACCESS", @@ -354,14 +403,28 @@ "signature": "NotRegistered(address)", "selector": "0xbfc6c337", "file": "contracts/example/Airdrop.sol", - "line": 60 + "line": 63 + }, + { + "name": "InvalidOfacRoots", + "signature": "InvalidOfacRoots()", + "selector": "0xc67a44d2", + "file": "contracts/IdentityVerificationHubImplV2.sol", + "line": 208 + }, + { + "name": "EXPIRY_IN_PAST", + "signature": "EXPIRY_IN_PAST()", + "selector": "0xca5d75dd", + "file": "contracts/registry/IdentityRegistryAadhaarImplV1.sol", + "line": 136 }, { "name": "CurrentDateNotInValidRange", "signature": "CurrentDateNotInValidRange()", "selector": "0xcf46551c", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 114 + "line": 140 }, { "name": "INVALID_VC_AND_DISCLOSE_PROOF", @@ -370,12 +433,19 @@ "file": "contracts/IdentityVerificationHubImplV1.sol", "line": 158 }, + { + "name": "AttestationIdMismatch", + "signature": "AttestationIdMismatch()", + "selector": "0xd7ca437d", + "file": "contracts/IdentityVerificationHubImplV2.sol", + "line": 204 + }, { "name": "InvalidVcAndDiscloseProof", "signature": "InvalidVcAndDiscloseProof()", "selector": "0xda7bd3a6", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 126 + "line": 152 }, { "name": "INVALID_REVEALED_DATA_TYPE", @@ -389,14 +459,14 @@ "signature": "ScopeMismatch()", "selector": "0xe7bee380", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 146 + "line": 172 }, { "name": "InvalidUserIdentifierInProof", "signature": "InvalidUserIdentifierInProof()", "selector": "0xebbcc178", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 162 + "line": 188 }, { "name": "CURRENT_DATE_NOT_IN_VALID_RANGE", @@ -410,13 +480,20 @@ "signature": "InvalidUserIdentifier()", "selector": "0xf0c426db", "file": "contracts/example/Airdrop.sol", - "line": 72 + "line": 75 }, { "name": "InvalidUserIdentifier", "signature": "InvalidUserIdentifier()", "selector": "0xf0c426db", "file": "contracts/example/SelfIdentityERC721.sol", + "line": 52 + }, + { + "name": "InvalidUserIdentifier", + "signature": "InvalidUserIdentifier()", + "selector": "0xf0c426db", + "file": "contracts/example/SelfPassportERC721.sol", "line": 49 }, { @@ -431,13 +508,13 @@ "signature": "InvalidIdentityCommitmentRoot()", "selector": "0xf53393a7", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 130 + "line": 156 }, { "name": "LengthMismatch", "signature": "LengthMismatch()", "selector": "0xff633a38", "file": "contracts/IdentityVerificationHubImplV2.sol", - "line": 106 + "line": 132 } ] diff --git a/packages/mobile-sdk-alpha/mobile-sdk-alpha.podspec b/packages/mobile-sdk-alpha/mobile-sdk-alpha.podspec index 3b26ba715..7549af0a3 100644 --- a/packages/mobile-sdk-alpha/mobile-sdk-alpha.podspec +++ b/packages/mobile-sdk-alpha/mobile-sdk-alpha.podspec @@ -1,26 +1,26 @@ -require 'json' +require "json" # Handle both local development and published package scenarios -package_json_path = File.join(__dir__, '..', 'package.json') +package_json_path = File.join(__dir__, "..", "package.json") if File.exist?(package_json_path) package = JSON.parse(File.read(package_json_path)) else # Fallback for when package.json is not found package = { - 'version' => '0.1.0', - 'description' => 'Self Mobile SDK Alpha' + "version" => "0.1.0", + "description" => "Self Mobile SDK Alpha", } end Pod::Spec.new do |s| - s.name = "mobile-sdk-alpha" - s.version = package['version'] - s.summary = package['description'] - s.homepage = "https://github.com/selfxyz/self" - s.license = "BUSL-1.1" - s.author = { "Self" => "team@self.xyz" } - s.platform = :ios, "13.0" - s.source = { :path => "." } + s.name = "mobile-sdk-alpha" + s.version = package["version"] + s.summary = package["description"] + s.homepage = "https://github.com/selfxyz/self" + s.license = "BUSL-1.1" + s.author = { "Self" => "support@self.xyz" } + s.platform = :ios, "13.0" + s.source = { :path => "." } s.source_files = "ios/**/*.{h,m,mm,swift}" s.public_header_files = "ios/**/*.h" @@ -29,13 +29,12 @@ Pod::Spec.new do |s| s.dependency "NFCPassportReader" s.pod_target_xcconfig = { - 'HEADER_SEARCH_PATHS' => '"$(PODS_ROOT)/Headers/Public/React-Core"', - 'DEFINES_MODULE' => 'YES', - 'SWIFT_INCLUDE_PATHS' => '$(PODS_ROOT)/mobile-sdk-alpha/ios' + "HEADER_SEARCH_PATHS" => '"$(PODS_ROOT)/Headers/Public/React-Core"', + "DEFINES_MODULE" => "YES", + "SWIFT_INCLUDE_PATHS" => "$(PODS_ROOT)/mobile-sdk-alpha/ios", } # Ensure iOS files are properly linked s.platform = :ios, "13.0" s.requires_arc = true - end diff --git a/packages/mobile-sdk-alpha/src/constants/analytics.ts b/packages/mobile-sdk-alpha/src/constants/analytics.ts index 0eb219806..fae69d751 100644 --- a/packages/mobile-sdk-alpha/src/constants/analytics.ts +++ b/packages/mobile-sdk-alpha/src/constants/analytics.ts @@ -2,9 +2,47 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. +export const AadhaarEvents = { + UPLOAD_SCREEN_OPENED: 'Aadhaar: Upload Screen Opened', + QR_UPLOAD_REQUESTED: 'Aadhaar: QR Upload Requested', + QR_UPLOAD_SUCCESS: 'Aadhaar: QR Upload Success', + QR_UPLOAD_FAILED: 'Aadhaar: QR Upload Failed', + PERMISSION_MODAL_OPENED: 'Aadhaar: Permission Modal Opened', + PERMISSION_MODAL_DISMISSED: 'Aadhaar: Permission Modal Dismissed', + PERMISSION_SETTINGS_OPENED: 'Aadhaar: Permission Settings Opened', + PROCESSING_STARTED: 'Aadhaar: Processing Started', + // Error-specific events + QR_CODE_EXPIRED: 'Aadhaar: QR Code Expired', + QR_CODE_INVALID_FORMAT: 'Aadhaar: QR Code Invalid Format', + QR_CODE_MISSING_FIELDS: 'Aadhaar: QR Code Missing Required Fields', + QR_CODE_PARSE_FAILED: 'Aadhaar: QR Code Parse Failed', + PHOTO_LIBRARY_UNAVAILABLE: 'Aadhaar: Photo Library Unavailable', + USER_CANCELLED_SELECTION: 'Aadhaar: User Cancelled Photo Selection', + // Validation events + TIMESTAMP_VALIDATION_STARTED: 'Aadhaar: Timestamp Validation Started', + TIMESTAMP_VALIDATION_FAILED: 'Aadhaar: Timestamp Validation Failed', + TIMESTAMP_VALIDATION_SUCCESS: 'Aadhaar: Timestamp Validation Success', + // Data processing events + QR_DATA_EXTRACTION_STARTED: 'Aadhaar: QR Data Extraction Started', + QR_DATA_EXTRACTION_SUCCESS: 'Aadhaar: QR Data Extraction Success', + DATA_STORAGE_STARTED: 'Aadhaar: Data Storage Started', + DATA_STORAGE_SUCCESS: 'Aadhaar: Data Storage Success', + // Screen interaction events + UPLOAD_BUTTON_DISABLED: 'Aadhaar: Upload Button Disabled', + UPLOAD_BUTTON_ENABLED: 'Aadhaar: Upload Button Enabled', + // Error recovery events + ERROR_SCREEN_NAVIGATED: 'Aadhaar: Error Screen Navigated', + RETRY_BUTTON_PRESSED: 'Aadhaar: Retry Button Pressed', + HELP_BUTTON_PRESSED: 'Aadhaar: Help Button Pressed', + // Success screen events + CONTINUE_TO_REGISTRATION_PRESSED: 'Aadhaar: Continue to Registration Pressed', +}; + export const AppEvents = { DISMISS_PRIVACY_DISCLAIMER: 'App: Dismiss Privacy Disclaimer', GET_STARTED: 'App: Get Started', + GET_STARTED_BIOMETRIC: 'App: Get Started - Biometric ID', + GET_STARTED_AADHAAR: 'App: Get Started - Aadhaar', SUPPORTED_BIOMETRIC_IDS: 'App: Supported Biometric IDs', UPDATE_MODAL_CLOSED: 'App: Update Modal Closed', UPDATE_MODAL_OPENED: 'App: Update Modal Opened', @@ -47,6 +85,7 @@ export const BackupEvents = { export const DocumentEvents = { ADD_NEW_MOCK_SELECTED: 'Document: Add New Document via Mock', ADD_NEW_SCAN_SELECTED: 'Document: Add New Document via Scan', + ADD_NEW_AADHAAR_SELECTED: 'Document: Add Aadhaar', DOCUMENT_DELETED: 'Document: Document Deleted', DOCUMENT_SELECTED: 'Document: Document Selected', DOCUMENTS_FETCHED: 'Document: Documents Fetched', diff --git a/packages/mobile-sdk-alpha/src/documents/utils.ts b/packages/mobile-sdk-alpha/src/documents/utils.ts index 18fb59618..64228717d 100644 --- a/packages/mobile-sdk-alpha/src/documents/utils.ts +++ b/packages/mobile-sdk-alpha/src/documents/utils.ts @@ -3,13 +3,15 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import { + AadhaarData, brutforceSignatureAlgorithmDsc, + isMRZDocument, parseCertificateSimple, PublicKeyDetailsECDSA, PublicKeyDetailsRSA, } from '@selfxyz/common'; import { calculateContentHash, inferDocumentCategory } from '@selfxyz/common/utils'; -import { DocumentMetadata, PassportData } from '@selfxyz/common/utils/types'; +import { DocumentMetadata, IDDocument } from '@selfxyz/common/utils/types'; import { SelfClient } from '../types/public'; @@ -38,11 +40,11 @@ export async function clearPassportData(selfClient: SelfClient) { export const getAllDocuments = async ( selfClient: SelfClient, ): Promise<{ - [documentId: string]: { data: PassportData; metadata: DocumentMetadata }; + [documentId: string]: { data: IDDocument; metadata: DocumentMetadata }; }> => { const catalog = await selfClient.loadDocumentCatalog(); const allDocs: { - [documentId: string]: { data: PassportData; metadata: DocumentMetadata }; + [documentId: string]: { data: IDDocument; metadata: DocumentMetadata }; } = {}; for (const metadata of catalog.documents) { @@ -77,7 +79,7 @@ export const hasAnyValidRegisteredDocument = async (client: SelfClient): Promise export const loadSelectedDocument = async ( selfClient: SelfClient, ): Promise<{ - data: PassportData; + data: IDDocument; metadata: DocumentMetadata; } | null> => { const catalog = await selfClient.loadDocumentCatalog(); @@ -122,11 +124,10 @@ export async function markCurrentDocumentAsRegistered(selfClient: SelfClient): P } } -export async function reStorePassportDataWithRightCSCA( - selfClient: SelfClient, - passportData: PassportData, - csca: string, -) { +export async function reStorePassportDataWithRightCSCA(selfClient: SelfClient, passportData: IDDocument, csca: string) { + if (passportData.documentCategory === 'aadhaar') { + return; + } const cscaInCurrentPassporData = passportData.passportMetadata?.csca; if (!(csca === cscaInCurrentPassporData)) { const cscaParsed = parseCertificateSimple(csca); @@ -156,7 +157,7 @@ export async function reStorePassportDataWithRightCSCA( export async function storeDocumentWithDeduplication( selfClient: SelfClient, - passportData: PassportData, + passportData: IDDocument, ): Promise { const contentHash = calculateContentHash(passportData); const catalog = await selfClient.loadDocumentCatalog(); @@ -181,11 +182,12 @@ export async function storeDocumentWithDeduplication( await selfClient.saveDocument(contentHash, passportData); // Add to catalog + const docType = passportData.documentType; const metadata: DocumentMetadata = { id: contentHash, - documentType: passportData.documentType, - documentCategory: passportData.documentCategory || inferDocumentCategory(passportData.documentType), - data: passportData.mrz || '', // Store MRZ for passports/IDs, relevant data for aadhaar + documentType: docType, + documentCategory: passportData.documentCategory || inferDocumentCategory(docType), + data: isMRZDocument(passportData) ? passportData.mrz : (passportData as AadhaarData).qrData || '', mock: passportData.mock || false, isRegistered: false, }; @@ -198,7 +200,7 @@ export async function storeDocumentWithDeduplication( return contentHash; } -export async function storePassportData(selfClient: SelfClient, passportData: PassportData) { +export async function storePassportData(selfClient: SelfClient, passportData: IDDocument) { await storeDocumentWithDeduplication(selfClient, passportData); } diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index 256f36e46..0af419d6a 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -108,7 +108,7 @@ export { reactNativeScannerAdapter } from './adapters/react-native/scanner'; export { scanQRProof } from './qr'; -export { useProtocolStore } from './stores/protocolStore'; +export { useProtocolStore, useSelfAppStore } from './stores'; // Error handling export { webScannerShim } from './adapters/web/shims'; diff --git a/packages/mobile-sdk-alpha/src/mock/generator.ts b/packages/mobile-sdk-alpha/src/mock/generator.ts index 4b6aff240..d9ba4c71f 100644 --- a/packages/mobile-sdk-alpha/src/mock/generator.ts +++ b/packages/mobile-sdk-alpha/src/mock/generator.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import type { IdDocInput } from '@selfxyz/common/utils'; +import type { IdDocInput, PassportData } from '@selfxyz/common/utils'; import { getSKIPEM } from '@selfxyz/common/utils/csca'; import { generateMockDSC, genMockIdDoc, initPassportDataParsing } from '@selfxyz/common/utils/passports'; @@ -12,17 +12,28 @@ export interface GenerateMockDocumentOptions { isInOfacList: boolean; selectedAlgorithm: string; selectedCountry: string; - selectedDocumentType: 'mock_passport' | 'mock_id_card'; + selectedDocumentType: 'mock_passport' | 'mock_id_card' | 'mock_aadhaar'; } const formatDateToYYMMDD = (date: Date): string => { return (date.toISOString().slice(2, 4) + date.toISOString().slice(5, 7) + date.toISOString().slice(8, 10)).toString(); }; -const getBirthDateFromAge = (age: number): string => { +// for aadhar +const formatDateToDDMMYYYY = (date: Date): string => { + return ( + date.toISOString().slice(8, 10) + + '-' + + date.toISOString().slice(5, 7) + + '-' + + date.toISOString().slice(0, 4) + ).toString(); +}; + +const getBirthDateFromAge = (age: number, format: 'YYMMDD' | 'DDMMYYYY' = 'YYMMDD'): string => { const date = new Date(); date.setFullYear(date.getFullYear() - age); - return formatDateToYYMMDD(date); + return format === 'YYMMDD' ? formatDateToYYMMDD(date) : formatDateToDDMMYYYY(date); }; const getExpiryDateFromYears = (years: number): string => { @@ -59,6 +70,23 @@ export async function generateMockDocument({ passportNumber: randomPassportNumber, }; + if (selectedDocumentType === 'mock_aadhaar') { + idDocInput.birthDate = getBirthDateFromAge(age, 'DDMMYYYY'); + + if (isInOfacList) { + idDocInput.lastName = 'HENAO MONTOYA'; + idDocInput.firstName = 'ARCANGEL DE JESUS'; + idDocInput.birthDate = '07-10-1954'; + } + + const result = genMockIdDoc(idDocInput); + if ('qrData' in result) { + console.log('Generated Aadhaar qrData:', result.qrData); + console.log('Generated Aadhaar extractedFields:', result.extractedFields); + } + return result; + } + let dobForGeneration: string; if (isInOfacList) { dobForGeneration = '541007'; @@ -78,7 +106,7 @@ export async function generateMockDocument({ rawMockData = genMockIdDoc(idDocInput); } const skiPem = await getSKIPEM('staging'); - return initPassportDataParsing(rawMockData, skiPem); + return initPassportDataParsing(rawMockData as PassportData, skiPem); } export const signatureAlgorithmToStrictSignatureAlgorithm = { diff --git a/packages/mobile-sdk-alpha/src/stores/protocolStore.ts b/packages/mobile-sdk-alpha/src/stores/protocolStore.ts index 266f1caf8..580655193 100644 --- a/packages/mobile-sdk-alpha/src/stores/protocolStore.ts +++ b/packages/mobile-sdk-alpha/src/stores/protocolStore.ts @@ -19,6 +19,8 @@ import { IDENTITY_TREE_URL_ID_CARD, IDENTITY_TREE_URL_STAGING, IDENTITY_TREE_URL_STAGING_ID_CARD, + TREE_URL, + TREE_URL_STAGING, } from '@selfxyz/common/constants'; import { fetchOfacTrees } from '@selfxyz/common/utils/ofac'; import type { DeployedCircuits, OfacTree } from '@selfxyz/common/utils/types'; @@ -58,6 +60,19 @@ interface ProtocolState { fetch_all: (environment: 'prod' | 'stg', ski: string) => Promise; fetch_ofac_trees: (environment: 'prod' | 'stg') => Promise; }; + aadhaar: { + commitment_tree: any; + public_keys: string[] | null; + deployed_circuits: DeployedCircuits | null; + circuits_dns_mapping: any; + ofac_trees: OfacTree | null; + fetch_deployed_circuits: (environment: 'prod' | 'stg') => Promise; + fetch_circuits_dns_mapping: (environment: 'prod' | 'stg') => Promise; + fetch_public_keys: (environment: 'prod' | 'stg') => Promise; + fetch_identity_tree: (environment: 'prod' | 'stg') => Promise; + fetch_all: (environment: 'prod' | 'stg') => Promise; + fetch_ofac_trees: (environment: 'prod' | 'stg') => Promise; + }; } export const useProtocolStore = create((set, get) => ({ @@ -318,4 +333,111 @@ export const useProtocolStore = create((set, get) => ({ } }, }, + aadhaar: { + commitment_tree: null, + public_keys: null, + deployed_circuits: null, + circuits_dns_mapping: null, + ofac_trees: null, + fetch_all: async (environment: 'prod' | 'stg') => { + try { + await Promise.all([ + get().aadhaar.fetch_deployed_circuits(environment), + get().aadhaar.fetch_circuits_dns_mapping(environment), + get().aadhaar.fetch_public_keys(environment), + get().aadhaar.fetch_identity_tree(environment), + get().aadhaar.fetch_ofac_trees(environment), + ]); + } catch (error) { + console.error(`Failed fetching Aadhaar data for ${environment}:`, error); + throw error; // Re-throw to let proving machine handle it + } + }, + fetch_deployed_circuits: async (environment: 'prod' | 'stg') => { + const url = `${environment === 'prod' ? API_URL : API_URL_STAGING}/deployed-circuits`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); + } + const responseText = await response.text(); + const data = JSON.parse(responseText); + set({ aadhaar: { ...get().aadhaar, deployed_circuits: data.data } }); + }, + fetch_circuits_dns_mapping: async (environment: 'prod' | 'stg') => { + const url = `${environment === 'prod' ? API_URL : API_URL_STAGING}/circuit-dns-mapping-gcp`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); + } + const responseText = await response.text(); + const data = JSON.parse(responseText); + set({ + aadhaar: { ...get().aadhaar, circuits_dns_mapping: data.data }, + }); + }, + fetch_public_keys: async (environment: 'prod' | 'stg') => { + const url = environment === 'prod' ? `${TREE_URL}/aadhaar-pubkeys` : `${TREE_URL_STAGING}/aadhaar-pubkeys`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); + } + const responseText = await response.text(); + const data = JSON.parse(responseText); + set({ aadhaar: { ...get().aadhaar, public_keys: data.data } }); + }, + fetch_identity_tree: async (environment: 'prod' | 'stg') => { + const url = `${environment === 'prod' ? TREE_URL : TREE_URL_STAGING}/identity-aadhaar`; + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error fetching ${url}! status: ${response.status}`); + } + const responseText = await response.text(); + const data = JSON.parse(responseText); + set({ aadhaar: { ...get().aadhaar, commitment_tree: data.data } }); + } catch (error) { + console.error(`Failed fetching Aadhaar identity tree from ${url}:`, error); + } + }, + fetch_ofac_trees: async (environment: 'prod' | 'stg') => { + const baseUrl = environment === 'prod' ? TREE_URL : TREE_URL_STAGING; + const nameDobUrl = `${baseUrl}/ofac/name-dob-aadhaar`; + const nameYobUrl = `${baseUrl}/ofac/name-yob-aadhaar`; + + try { + const fetchTree = async (url: string): Promise => { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`HTTP error fetching ${url}! status: ${res.status}`); + } + const responseData = await res.json(); + + if (responseData && typeof responseData === 'object' && 'status' in responseData) { + if (responseData.status !== 'success' || !responseData.data) { + throw new Error(`Failed to fetch tree from ${url}: ${responseData.message || 'Invalid response format'}`); + } + return responseData.data; + } + + return responseData; + }; + + const [nameDobData, nameYobData] = await Promise.all([fetchTree(nameDobUrl), fetchTree(nameYobUrl)]); + + set({ + aadhaar: { + ...get().aadhaar, + ofac_trees: { + passportNoAndNationality: null, + nameAndDob: nameDobData, + nameAndYob: nameYobData, + }, + }, + }); + } catch (error) { + console.error('Failed fetching Aadhaar OFAC trees:', error); + set({ aadhaar: { ...get().aadhaar, ofac_trees: null } }); + } + }, + }, })); diff --git a/packages/mobile-sdk-alpha/src/types/public.ts b/packages/mobile-sdk-alpha/src/types/public.ts index 076202416..0fc120c4b 100644 --- a/packages/mobile-sdk-alpha/src/types/public.ts +++ b/packages/mobile-sdk-alpha/src/types/public.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import type { DocumentCatalog, PassportData } from '@selfxyz/common/utils/types'; +import type { DocumentCatalog, IDDocument, PassportData } from '@selfxyz/common/utils/types'; import { SDKEvent, SDKEventMap } from './events'; @@ -153,8 +153,8 @@ export interface DocumentsAdapter { loadDocumentCatalog(): Promise; saveDocumentCatalog(catalog: DocumentCatalog): Promise; - loadDocumentById(id: string): Promise; - saveDocument(id: string, passportData: PassportData): Promise; + loadDocumentById(id: string): Promise; + saveDocument(id: string, passportData: IDDocument): Promise; deleteDocument(id: string): Promise; } @@ -171,8 +171,8 @@ export interface SelfClient { loadDocumentCatalog(): Promise; saveDocumentCatalog(catalog: DocumentCatalog): Promise; - loadDocumentById(id: string): Promise; - saveDocument(id: string, passportData: PassportData): Promise; + loadDocumentById(id: string): Promise; + saveDocument(id: string, passportData: IDDocument): Promise; deleteDocument(id: string): Promise; }