Attendance Sanchalan
Sanchalan Docs
Biometric + Aadhaar

Biometric + Aadhaar identity binding

The current scaffold proves the device was at the right place, signing with a device-bound key. It does not yet prove the person holding the device is the registered employee. NIC's AEBAS (attendance.gov.in) closes that gap with Aadhaar fingerprint or face authentication against the UIDAI database. This page lays out how Attendance Sanchalan reaches that bar.

What we have today vs. what we need

PropertyToday (Sanchalan v0.1)AEBAS (NIC)Sanchalan target
Device-at-location proof✅ P-256 ECDSA punch signature + geofence + Wi-Fi SSIDRD-device tied to terminal locationKeep.
Anti-spoof signals✅ mock_location / rooted / emulator (soft)RD device with hardware livenessAdd Play Integrity / DeviceCheck.
Person identity binding❌ none — assumes device == employee✅ Aadhaar 1:1 face/fingerprint match (UIDAI yes/no)Layered, see below.
Liveness detection✅ Face RD passive PAD + UIDAI sign-offUIDAI Face RD (primary) + ML Kit fallback.
Cryptographic non-repudiation✅ per-punch ECDSA signatureRD-device-signed PID blockKeep both.
Audit chainsds_audit hash-chainedNIC AEBAS audit (private)Keep.

The four-layer model

No single signal is sufficient. We layer them so the system fails closed if any one is compromised, and so we can reach Aadhaar-grade assurance without paying that latency cost on every punch.

flowchart TB subgraph L1["Layer 1 — Device binding (today)"] L1a[P-256 keypair in secure enclave] L1b[Per-punch ECDSA signature] L1c[Device UUID + sha256-salted installation hash] end subgraph L2["Layer 2 — Location + environment (today)"] L2a[GPS polygon containment] L2b[Wi-Fi SSID allowlist] L2c[Anti-spoof signals] end subgraph L3["Layer 3 — On-device person binding (Phase 1, no UIDAI)"] L3a[Android BiometricPrompt / iOS LocalAuthentication] L3b[Selfie capture + on-device liveness ML Kit] L3c[Encrypt + store as evidence — not identity claim] end subgraph L4["Layer 4 — Aadhaar identity proof (Phase 3, requires AUA license)"] L4a[UIDAI Face RD intent on Android] L4b[Encrypted PID block to AUA backend] L4c[UIDAI Auth API 2.5 — yes/no + auth_code] end L1 --> Punch[every punch] L2 --> Punch L3 -. each punch .-> Punch L4 -. first IN of day / committee meetings .-> Punch

Phased rollout — what's feasible when

PhaseCapabilityUIDAI dependencyBuild effort
0 (today)Device + location + cryptononeshipped
1+ on-device biometric prompt before punch (fingerprint / face the user previously enrolled on the phone)none~2 days mobile + 1 day backend (record bio_used flag)
2+ selfie capture + ML Kit passive liveness, encrypted evidence storagenone~1 week mobile + storage layer
3+ UIDAI Face Auth via Face RD APK + AUA licenseAUA / Sub-AUA license from UIDAI (typically via NIC sub-licensing)~3 weeks engineering + license timeline (months)
4+ hardware attestation (Play Integrity / DeviceCheck) replacing soft anti-spoofGoogle Play Console + Apple Developer Program~1 week + procurement

Phase 1 — on-device biometric (implementable now)

Android BiometricPrompt and iOS LocalAuthentication let us require the user to present an enrolled fingerprint or face before each punch. The biometric never leaves the secure enclave; the OS returns yes/no.

This is not Aadhaar identity proof, but it does prove "the person who enrolled biometric on this device tapped the punch button" — a meaningful upgrade over "someone holding the device tapped". Combined with one-time device registration done in person at HR (operator verifies the employee's Aadhaar at registration), it gives a defensible chain.

sequenceDiagram participant U as Employee participant App as Flutter app participant SE as Android KeyStore / iOS Secure Enclave participant Be as backend U->>App: tap "Punch IN" App->>SE: BiometricPrompt.authenticate(
title="Confirm punch", reason="...") SE->>U: show fingerprint / face prompt U->>SE: present biometric SE->>SE: match against enrolled template (in TEE/SE) alt match SE-->>App: success — release sign() capability App->>SE: sign(nonce + uuid + punched_at) App->>Be: POST /v1/punch + bio_used=true + bio_method=fingerprint Be->>Be: existing flow + record bio_used in attendance_punches else no match SE-->>App: error (cancelled / lockout / no enrollment) App->>U: "Biometric required" end

Backend changes for Phase 1 (small):

// migration: add nullable bio_used + bio_method to attendance_punches
$table->boolean('bio_used')->default(false);
$table->string('bio_method', 16)->nullable();   // fingerprint | face | iris | none

// PunchController validate:
'bio_used'   => ['nullable', 'boolean'],
'bio_method' => ['nullable', Rule::in(['fingerprint','face','iris'])],

Mobile changes for Phase 1:

  • Add local_auth: ^2.3 to pubspec.yaml.
  • Wrap PunchKeypair.signRaw() in a LocalAuthentication.authenticate() call.
  • Bind the keypair to require user authentication in Android KeyStore — so even a rooted attacker can't sign without the biometric.

Phase 2 — selfie + on-device liveness

For staff cohorts where Aadhaar isn't available (visiting members, foreign delegations) or as evidence for dispute resolution, we capture a punch-time selfie with an active liveness challenge.

  • Liveness: google_mlkit_face_detection (free) detects head pose, eye-blink probability, and smile probability. Active challenge: "blink twice", "turn head left", random per punch — defeats simple photo replay.
  • Storage: encrypted blob (AES-GCM with Vault-issued DEK), stored under an attendance_punch_selfies table referencing the punch row. Never displayed to other employees; admin RBAC-gated, audit-logged on access.
  • Retention: 90 days unless attached to a dispute. Punch row keeps a hash of the encrypted blob for tamper detection.

This is not identity authentication — there's nothing to match against — but it's photographic proof that someone roughly resembling the registered employee was at the location. Useful for HR investigation, not for automated approve/reject.

Phase 3 — UIDAI Face Auth (the AEBAS-equivalent)

What UIDAI Face Auth actually is

UIDAI provides a "Face RD" service distributed as a separate Android APK (and iOS framework) that the user installs alongside our app. Our app launches it via Intent; it captures the face, runs hardware-assisted liveness (passive — no challenge needed), packages an encrypted PID block, and returns it. We forward the PID to the AUA endpoint over HTTPS; UIDAI returns a yes/no with an auth_code.

sequenceDiagram participant U as Employee participant App as Sanchalan app participant FRD as UIDAI Face RD APK participant AUA as Sanchalan backend (AUA role) participant ASA as ASA gateway participant UIDAI as UIDAI Auth Server U->>App: punch with Aadhaar (first IN of day) App->>App: collect Aadhaar number + consent flag App->>FRD: Intent ACTION_FACE_RD
(extras: aadhaar_last_4, consent) FRD->>U: open camera, capture + liveness FRD->>FRD: passive PAD (motion + texture) FRD-->>App: encrypted PID block (XML, signed by RD private key) App->>AUA: POST /v1/punch/aadhaar
{aadhaar_hash, pid_xml, device_punch_payload} AUA->>AUA: validate device punch (existing flow) AUA->>AUA: wrap PID with our AUA license + sign Auth XML AUA->>ASA: HTTPS POST signed Auth XML ASA->>UIDAI: forward UIDAI-->>ASA: yes/no + auth_code + ts ASA-->>AUA: response AUA->>AUA: persist punch with verdict
+ aadhaar_auth_code (for forensic, not the number) AUA-->>App: { verdict: accepted_aadhaar_face, auth_code }

What needs to be in place before Phase 3 ships

  1. AUA license: Rajya Sabha applies to UIDAI as Sub-AUA (typically under NIC's parent AUA license, which already serves AEBAS). Application includes:
    • Use case: parliamentary attendance, employees + members.
    • Privacy Impact Assessment (PIA).
    • Data flow diagram (the one above).
    • Information Security Audit Report (ISO 27001 / equivalent).
    • Annual external audit commitment.
    Timeline: typically 4–8 weeks once paperwork is complete.
  2. ASA (Authentication Service Agency): contract with one of the UIDAI-empanelled ASAs (NSDL, BSNL, etc.) for the network leg into UIDAI's authentication server. They host the licensed endpoint and transport our signed Auth XML.
  3. HSM / Vault for AUA signing key: the AUA license private key must be stored in a FIPS 140-2 L2+ HSM or equivalent. Vault transit engine with auto-unseal qualifies; SDS Vault setup already in place per /infra/.
  4. Annual audit: budgeted; typically ₹5–10L per year for the audit + license renewal.

Legal / compliance constraints

  • Aadhaar Act §7 + Puttaswamy 2018: Aadhaar auth requires the activity to be a "service" or "subsidy". Attendance is borderline — defensible if framed as access control to government workplace. Legal opinion required before deployment.
  • Voluntariness: an alternative non-Aadhaar fallback path is mandatory. Our existing device + geofence + Phase-1 biometric flow is that fallback.
  • Data minimisation (Aadhaar Act §29): we never store the raw Aadhaar number. Only:
    • SHA-256 hash of Aadhaar (with per-deployment salt) — for de-duplication and lookup.
    • UIDAI auth_code (the response identifier) — for forensic correlation if UIDAI requests it.
    • Encrypted consent record (with consent text version + timestamp).
  • Consent: explicit, informed, separate from app login. Recorded in attendance_aadhaar_consent table per employee per consent-text-version.
  • DPDP Act 2023: data fiduciary obligations — privacy policy URL, grievance officer, breach notification within 72 h.

What we encrypt and store (Aadhaar layer)

DataWhere it goesEncryption
Raw Aadhaar numberNever persisted. In-memory only during one auth call.
Aadhaar SHA-256 hashattendance_aadhaar_consent.aadhaar_hash + linked to users.idSalt in Vault.
PID blockTransmitted to UIDAI, never persisted.UIDAI public key envelope.
UIDAI auth_codeattendance_punches.aadhaar_auth_code (nullable)
Selfie (Phase 2 only)attendance_punch_selfies.encrypted_blobAES-256-GCM, DEK from Vault transit, KEK in Vault HSM.
Consent recordattendance_aadhaar_consent

Phase 4 — hardware attestation

  • Android Play Integrity API replaces our soft rooted_or_jailbroken + emulator signals with a Google-signed integrity verdict. We forward the Play Integrity token with each punch; backend verifies with Google's public key. Defeats most root cloak / Magisk modules.
  • iOS DeviceCheck + App Attest: equivalent for iOS — App Attest cryptographically asserts the binary is genuine and unmodified, signed by Apple.
  • Cost: Play Integrity is free for moderate volumes. App Attest free with Apple Developer Program ($99/year).

Concrete data-model additions

The following migrations would land alongside Phase 1 / Phase 3:

// Phase 1 — biometric flag on punches
ALTER TABLE attendance_punches
  ADD COLUMN bio_used     boolean NOT NULL DEFAULT false,
  ADD COLUMN bio_method   varchar(16);   -- fingerprint | face | iris

// Phase 3 — Aadhaar auth + consent
CREATE TABLE attendance_aadhaar_consent (
  id                bigserial PRIMARY KEY,
  user_id           bigint REFERENCES users(id),
  employee_id       bigint REFERENCES attendance_employee_register(id),
  aadhaar_hash      varchar(64) NOT NULL,            -- sha256(aadhaar + vault_salt)
  consent_version   varchar(16) NOT NULL,
  consent_text_url  varchar(255) NOT NULL,
  consented_at      timestamptz NOT NULL,
  withdrawn_at      timestamptz,
  ip_address        inet,
  user_agent        text,
  UNIQUE(user_id, consent_version, withdrawn_at)
);

ALTER TABLE attendance_punches
  ADD COLUMN aadhaar_auth_code  varchar(64),         -- UIDAI response identifier
  ADD COLUMN aadhaar_verified   boolean,
  ADD COLUMN identity_assurance varchar(8);          -- l1 (device) | l2 (device+bio) | l3 (Aadhaar)

// Phase 2 — selfie evidence
CREATE TABLE attendance_punch_selfies (
  id              bigserial PRIMARY KEY,
  punch_id        bigint REFERENCES attendance_punches(id) ON DELETE CASCADE,
  encrypted_blob  bytea NOT NULL,                    -- AES-256-GCM, DEK in Vault
  blob_sha256     varchar(64) NOT NULL,
  liveness_score  decimal(3,2),                      -- 0..1
  liveness_method varchar(32),                       -- mlkit | facetec | uidai_rd
  retention_until date NOT NULL DEFAULT (now() + interval '90 days'),
  created_at      timestamptz DEFAULT now()
);

Verdict expansion

The verdict column will gain values to capture identity assurance:

VerdictMeaning
acceptedPhase 0 — device + location only.
accepted_bioPhase 1 — device biometric matched.
accepted_aadhaar_facePhase 3 — UIDAI Face Auth yes.
accepted_aadhaar_fingerPhase 3 — UIDAI Fingerprint Auth yes.
rejected_biometricBiometricPrompt cancelled / lockout.
rejected_aadhaarUIDAI returned no.
rejected_livenessOn-device liveness failed.

The pragmatic path

  1. Now — implement Phase 1 (on-device BiometricPrompt). Two days of work. Closes 80% of the "wrong person on the device" attack surface.
  2. Operator action — start the AUA license application via NIC. The 4–8 week clock starts now.
  3. While waiting on UIDAI — build the Aadhaar integration end-to-end against a UIDAI sandbox (they offer a staging endpoint to license-applicants). Test the full PID flow before production license arrives.
  4. License received — flip AUTH_AADHAAR_ENABLED=true, route Phase-3 punches through. Existing Phase 0/1 flows remain available for fallback.
  5. Phase 4 — Play Integrity / App Attest in parallel, no UIDAI dependency.

Open questions for the deployment team

  • Does Rajya Sabha already hold an AUA license, or are we applying fresh? (Determines timeline.)
  • Is the use-case framing "access control" or "service delivery"? (Affects §7 applicability and legal sign-off.)
  • Mandatory vs. opt-in Aadhaar? (Mandatory needs explicit notification per Aadhaar Act §7; opt-in is straightforward but means many staff stay on Phase 1.)
  • For members of parliament — Aadhaar use for parliamentary attendance has not been litigated; expect legal review before enabling.
  • Retention policy for selfies (Phase 2) — 90 days default; finance/HR teams may want longer for payroll dispute resolution.