package com.rdservice import android.app.Activity import android.content.Intent import android.util.Log import com.facebook.react.bridge.ActivityEventListener import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.BaseActivityEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableMap import com.facebook.react.module.annotations.ReactModule @ReactModule(name = RdserviceModule.NAME) class RdserviceModule(reactContext: ReactApplicationContext) : NativeRdserviceSpec(reactContext) { companion object { const val NAME = "Rdservice" private const val RDINFO_CODE = 1 private const val RDCAPTURE_CODE = 2 private const val IRIS_CAPTURE_CODE = 3 private const val FACE_CAPTURE_CODE = 9 private const val ACTION_FP_INFO = "in.gov.uidai.rdservice.fp.INFO" private const val ACTION_FP_CAPTURE = "in.gov.uidai.rdservice.fp.CAPTURE" private const val ACTION_IRIS_CAPTURE = "in.gov.uidai.rdservice.iris.CAPTURE" private const val ACTION_FACE_CAPTURE = "in.gov.uidai.rdservice.face.CAPTURE" private const val SUCCESS = "SUCCESS" private const val FAILURE = "FAILURE" // RD service responses shorter than this cannot contain valid XML. private const val MIN_VALID_RESPONSE_LENGTH = 10 // Valid Android application id, e.g. com.mantra.rdservice. Rejecting anything // else prevents the capture intent from being sent as an implicit broadcast // that an arbitrary app could answer. private val PACKAGE_NAME_PATTERN = Regex("^[a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z][a-zA-Z0-9_]*)+$") private const val DEFAULT_FINGER_PID_OPTIONS = """""" private const val DEFAULT_IRIS_PID_OPTIONS = """""" // The wadh value below is the UIDAI-published constant for face authentication. // Pass custom PidOptions for eKYC or other flows. private const val DEFAULT_FACE_PID_OPTIONS = """ """ } private var promise: Promise? = null private val activityEventListener: ActivityEventListener = object : BaseActivityEventListener() { override fun onActivityResult( activity: Activity, requestCode: Int, resultCode: Int, data: Intent?, ) { when (requestCode) { RDINFO_CODE, RDCAPTURE_CODE, IRIS_CAPTURE_CODE, FACE_CAPTURE_CODE, -> Unit // Results from unrelated activities must not touch a pending capture. else -> return } if (data == null) { resolve(FAILURE, "No action taken") return } when (requestCode) { RDINFO_CODE -> handleInfoResult(data.getStringExtra("RD_SERVICE_INFO")) RDCAPTURE_CODE, IRIS_CAPTURE_CODE -> handleCaptureResult(data.getStringExtra("PID_DATA")) FACE_CAPTURE_CODE -> handleCaptureResult(data.getStringExtra("response")) } } } init { reactContext.addActivityEventListener(activityEventListener) } override fun getName(): String = NAME override fun invalidate() { reactApplicationContext.removeActivityEventListener(activityEventListener) promise = null super.invalidate() } override fun getDeviceInfo(deviceName: String, promise: Promise) { startRdActivity(promise, deviceName, RDINFO_CODE) { Intent(ACTION_FP_INFO) } } override fun getFingerPrint(deviceName: String, pidOption: String, promise: Promise) { startRdActivity(promise, deviceName, RDCAPTURE_CODE) { Intent(ACTION_FP_CAPTURE) .putExtra("PID_OPTIONS", pidOptionOrDefault(pidOption, DEFAULT_FINGER_PID_OPTIONS)) } } override fun getIrisCapture(deviceName: String, pidOption: String, promise: Promise) { startRdActivity(promise, deviceName, IRIS_CAPTURE_CODE) { Intent(ACTION_IRIS_CAPTURE) .putExtra("PID_OPTIONS", pidOptionOrDefault(pidOption, DEFAULT_IRIS_PID_OPTIONS)) } } override fun getFaceCapture(deviceName: String, pidOption: String, promise: Promise) { startRdActivity(promise, deviceName, FACE_CAPTURE_CODE) { Intent(ACTION_FACE_CAPTURE) .putExtra("request", pidOptionOrDefault(pidOption, DEFAULT_FACE_PID_OPTIONS)) } } private fun pidOptionOrDefault(pidOption: String, default: String): String = if (pidOption.trim().length >= MIN_VALID_RESPONSE_LENGTH) pidOption else default private fun startRdActivity( promise: Promise, packageName: String, requestCode: Int, intentBuilder: () -> Intent, ) { if (this.promise != null) { resolvePromise(promise, FAILURE, "Another RD service request is already in progress") return } if (!PACKAGE_NAME_PATTERN.matches(packageName)) { resolvePromise(promise, FAILURE, "Invalid RD service package name") return } val activity = reactApplicationContext.getCurrentActivity() if (activity == null) { resolvePromise(promise, FAILURE, "No foreground activity available to start RD service") return } this.promise = promise try { activity.startActivityForResult(intentBuilder().setPackage(packageName), requestCode) } catch (e: Exception) { // Log only the exception type; never log intent contents or biometric data. Log.w(NAME, "Unable to start RD service activity: ${e.javaClass.simpleName}") resolve(FAILURE, "Selected device not found") } } private fun handleInfoResult(info: String?) { if (info == null || info.length <= MIN_VALID_RESPONSE_LENGTH || info.lowercase().contains("notready") ) { resolve(FAILURE, "Device not ready") return } resolve(SUCCESS, info) } private fun handleCaptureResult(captureXml: String?) { if (captureXml == null || captureXml.length <= MIN_VALID_RESPONSE_LENGTH) { resolve(FAILURE, "Device not ready") return } if (captureXml.lowercase().contains("device not ready")) { resolve(FAILURE, "Device not ready") return } // Return the PID XML exactly as the RD service produced it. It is digitally // signed; altering even a single character breaks server-side verification. resolve(SUCCESS, captureXml) } private fun resolve(status: String, message: String) { val pending = promise ?: return promise = null resolvePromise(pending, status, message) } private fun resolvePromise(promise: Promise, status: String, message: String) { val map: WritableMap = Arguments.createMap() map.putString("status", status) map.putString("message", message) promise.resolve(map) } }