Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
fe76b75
feat(aadhaar-vault): Session 1 bootstrap — Fastify 4 + Pino + Vitest …
DUKartik Jul 21, 2026
26dde84
feat(aadhaar-vault): Session 2 - in-memory MemoryPool + Postgres adap…
Jul 21, 2026
74bb51b
feat(aadhaar-vault): Session 3 - KeyManager port + LocalDevKeyManager…
DUKartik Jul 21, 2026
cd32d5f
test(aadhaar-vault): add focused unit tests for safeZero()
DUKartik Jul 22, 2026
8e616bb
feat(aadhaar-vault): POST /v1/tokenize route, application command, an…
DUKartik Jul 22, 2026
1498287
Merge branch 'main' of https://github.com/DUKartik/FLN_Open_Source in…
DUKartik Jul 22, 2026
16609c3
feat(aadhaar-vault): Session 5 Phase 1 — Auth foundation (Bearer JWT …
DUKartik Jul 24, 2026
32d62ba
docs(REPO_STATUS): sync to Session 5 Phase 2 — MFA factor model + TOT…
DUKartik Jul 24, 2026
926faaa
feat(aadhaar-vault): Session 5 Phase 2 — MFA factor model + TOTP veri…
DUKartik Jul 24, 2026
65029cb
feat(aadhaar-vault): Session 5C application commands
DUKartik Jul 24, 2026
ec36d13
feat(aadhaar-vault): Session 5C application commands
DUKartik Jul 24, 2026
149d09f
feat(aadhaar-vault): Session 6A detokenize route (POST /v1/tokenize/:…
DUKartik Jul 25, 2026
36bc743
feat(aadhaar-vault): Session 6B audit history HTTP route
DUKartik Jul 25, 2026
79dddaa
feat(aadhaar-vault): Session 6C MFA enrollment HTTP route
DUKartik Jul 25, 2026
bae50e6
feat(aadhaar-vault): Session 6D MFA verification HTTP route
DUKartik Jul 27, 2026
e6c45a3
feat(aadhaar-vault): migrate database added
DUKartik Jul 27, 2026
ec1c9ee
fix(aadhaar-vault):fix cors and port error
DUKartik Jul 27, 2026
b5257bf
feat(step-up): complete Session 7 secure detokenization workflow
DUKartik Jul 31, 2026
96b2e8c
console based detonkenization completed
DUKartik Jul 31, 2026
ecef0e6
fix(console): tokenization bug fixed
DUKartik Aug 1, 2026
be779a7
main merged and remaining md file
DUKartik Aug 1, 2026
c66eae3
readme and other md file update
DUKartik Aug 1, 2026
698d1f9
Merge branch 'main' of https://github.com/DUKartik/FLN_Open_Source in…
DUKartik Aug 21, 2026
5a2e517
Merge branch 'main' of https://github.com/DUKartik/FLN_Open_Source in…
DUKartik Aug 21, 2026
6533e4f
integration phase1 done
DUKartik Aug 22, 2026
50fa99c
feat: implement Phase 2 hardening for Aadhaar Vault integration
DUKartik Aug 22, 2026
cd19394
feat(aadhaar-vault): wire Step-Up detokenization into admin workflow
DUKartik Aug 27, 2026
f749a13
Merge origin/main into feature/aadhaar-vault
DUKartik Aug 28, 2026
6b72b05
fix(git): resolve package.json merge conflict + strip .gitignore BOM
DUKartik Sep 1, 2026
701582e
feat(vault): Phase 1 module skeleton with feature flag
DUKartik Sep 1, 2026
d3d5475
feat(aadhaar-vault): in-process tokenize module — Phase 2
DUKartik Sep 1, 2026
668899a
feat(aadhaar-vault): Phase 3 — detokenize + audit history + step-up CAS
DUKartik Sep 1, 2026
9be2014
feat(aadhaar-vault): Phase 4 — in-process step-up + MFA + JWT verifier
DUKartik Sep 1, 2026
0c29197
feat(aadhaar-vault): Phase 6 — graceful shutdown with vault transacti…
DUKartik Sep 1, 2026
af9c9ed
feat(aadhaar-vault): Phase 7 — delete microservices/aadhaar-vault
DUKartik Sep 1, 2026
c30b8c1
vault(audit): unify audit chain on FLN logbook (issue #406)
DUKartik Sep 1, 2026
49238e0
feat: Enhance Aadhaar Reveal Panel with server-side pagination and se…
DUKartik Sep 3, 2026
033690c
feat: add SecurityPanel component for authenticator enrollment and ma…
DUKartik Sep 3, 2026
b8e0ab1
feat: update TOTP code prompts to accept 6- or 8-digit codes and enha…
DUKartik Sep 3, 2026
d4280e3
feat: enhance Aadhaar Reveal panel with case-insensitive search index…
DUKartik Sep 4, 2026
a404a8e
Merge branch 'main' of https://github.com/DUKartik/FLN_Open_Source in…
DUKartik Sep 4, 2026
eaae9dd
feat: add LOCAL_DEV_MASTER_KEY for local development mode in Aadhaar …
DUKartik Sep 4, 2026
663cb5a
Merge origin/main into feature/aadhaar-vault
DUKartik Sep 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Archive and outputs
# Archive and outputs
archive/
mvp/
output/
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,39 @@ Demo login after seeding: `superadmin@fln.org`, password `Fln@2026` (see
emails, which follow a predictable `role.<state>_<district>_<block>_<school>@fln.org`
pattern).

### Aadhaar tokenization (in-process vault)

Student registration tokenizes the 12-digit Aadhaar through the in-process
vault module at [`backend/src/modules/vault/`](backend/src/modules/vault/) —
the FLN backend never stores plaintext Aadhaar and never exposes Vault
service JWTs to the browser (see
[`backend/src/aadhaarVault.ts`](backend/src/aadhaarVault.ts) and
[`backend/src/routes/students.ts`](backend/src/routes/students.ts)). The
module is wired unconditionally at boot; no feature flag, no separate
process, no service-JWT exchange.

The module needs two env vars (both required for tokenization to succeed):

- `MONGODB_URI` — the FLN backend's existing Mongo connection. The vault
reuses the same replica set; the module fails fast with
`VAULT_DB_REQUIRES_REPLICA_SET` (503) if pointed at a standalone
`mongod` because `session.withTransaction(...)` is unsupported there.
- `LOCAL_DEV_MASTER_KEY` — base64; ≥ 32 decoded bytes. The
per-record DEK wrap subkey is derived from this via
`HKDF-SHA-256(master, salt=context, info="aadhaar-vault/dek-wrap")`.
Production deployments are expected to swap `LocalDevKeyManager` for a
real KMS provider; the port is stable.

Until both are set, `POST /api/students` and `POST /api/students/bulk-import`
fail with `VaultError NOT_CONFIGURED` (by design — no plaintext fallback).

End-to-end contract is enforced by the integration test suite at
[`backend/tests/aadhaar-hardening.test.ts`](backend/tests/aadhaar-hardening.test.ts)
and [`backend/tests/aadhaar-detokenize.test.ts`](backend/tests/aadhaar-detokenize.test.ts)
(run with `npm test` from `backend/`), and by the read-only at-rest audit
at [`backend/scripts/audit-aadhaar-at-rest.ts`](backend/scripts/audit-aadhaar-at-rest.ts)
(`npm run audit:aadhaar`).

## Rules
Contributor Onboarding — Onboarding Document (Mandatory)

Expand Down
6 changes: 6 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,9 @@ MISCONCEPTION_AI_NAMING=off
# (plus its system libraries on Linux). Leave CHROME_EXECUTABLE_PATH unset to use
# that managed browser, or point it at a specific Chrome/Chromium binary.
# CHROME_EXECUTABLE_PATH=/usr/bin/chromium


# important for running addhar vault in local dev mode. Set to any random string to enable local dev mode.

LOCAL_DEV_MASTER_KEY= udggfuiegljvhvheuvljhbcwoskjdbfsbfsajbsjsbsaj
# This is a security risk if you run the server on a public network, so don't set it in production.
7 changes: 7 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
"seed:question-bank": "tsx src/seedQuestionBank.ts",
"seed:geo": "tsx src/utils/seedGeo.ts",
"reseed": "tsx src/reseed.ts",
"test": "esbuild tests/aadhaar-hardening.test.ts --bundle --platform=node --format=esm --packages=external --outfile=tests/dist/aadhaar-hardening.test.mjs --log-level=warning && node tests/dist/aadhaar-hardening.test.mjs && esbuild tests/aadhaar-detokenize.test.ts --bundle --platform=node --format=esm --packages=external --outfile=tests/dist/aadhaar-detokenize.test.mjs --log-level=warning && node tests/dist/aadhaar-detokenize.test.mjs && tsx --test src/modules/vault/tests/transaction-counter.test.ts && tsx --test src/modules/vault/tests/step-up-e2e.test.ts && tsx --test src/modules/vault/tests/audit-in-logbook.test.ts && tsx --test tests/panel-isolation.test.cjs",
"test:detokenize": "esbuild tests/aadhaar-detokenize.test.ts --bundle --platform=node --format=esm --packages=external --outfile=tests/dist/aadhaar-detokenize.test.mjs --log-level=warning && node tests/dist/aadhaar-detokenize.test.mjs",
"test:search-index": "tsx --test tests/students-search-index.test.ts",
"test:panel-isolation": "tsx --test tests/panel-isolation.test.cjs",
"audit:aadhaar": "tsx scripts/audit-aadhaar-at-rest.ts",
"test:answer-matching": "tsx src/answerMatching.test.ts",
"test:paper-lock": "tsx src/paperLock.test.ts"
},
Expand All @@ -26,9 +31,11 @@
"dotenv": "^17.2.3",
"express": "^4.21.2",
"express-rate-limit": "^8.6.1",
"jose": "5.9.6",
"jsonwebtoken": "^9.0.2",
"jszip": "^3.10.1",
"mongodb": "^6.12.0",
"otpauth": "^9.5.1",
"pdf-lib": "^1.17.1",
"puppeteer": "^25.3.0"
},
Expand Down
106 changes: 106 additions & 0 deletions backend/scripts/audit-aadhaar-at-rest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Read-only Aadhaar-at-rest audit (Phase 2 hardening).
//
// Answers, with totals only:
// 1. How many student docs appear to contain RAW Aadhaar?
// 2. How many docs lack aadhaarIdentityId?
// 3. How many docs are already fully tokenized?
// 4. Any unexpected Aadhaar-related fields?
//
// Guarantees:
// - READ-ONLY: no document is created, modified, or deleted.
// - Never prints Aadhaar values, tokens, or identity ids.
// - Never calls detokenization or any vault endpoint.
//
// Usage:
// cd backend
// MONGODB_URI=... npx tsx scripts/audit-aadhaar-at-rest.ts # audit MongoDB
// npx tsx scripts/audit-aadhaar-at-rest.ts # audit local file store
import 'dotenv/config';
import { connectDB, dbStore } from '../src/db';

const KNOWN_AADHAAR_FIELDS = new Set(['aadharMasked', 'aadhaarTokenId', 'aadhaarIdentityId']);

interface AuditTally {
total: number;
rawMaskExact: number; // aadharMasked is exactly 12 digits → almost certainly raw
rawMaskEmbedded: number; // aadharMasked contains a 12-digit run but is not exactly 12 digits
missingIdentityId: number; // no aadhaarIdentityId
fullyTokenized: number; // mask shape + token + identityId all present
unexpectedFields: Record<string, number>;
}

function newTally(): AuditTally {
return { total: 0, rawMaskExact: 0, rawMaskEmbedded: 0, missingIdentityId: 0, fullyTokenized: 0, unexpectedFields: {} };
}

function tallyDoc(t: AuditTally, doc: any): void {
t.total += 1;
const mask = typeof doc?.aadharMasked === 'string' ? doc.aadharMasked : '';
if (/^\d{12}$/.test(mask)) {
t.rawMaskExact += 1;
} else if (/\d{12}/.test(mask)) {
t.rawMaskEmbedded += 1;
}
const hasIdentity = typeof doc?.aadhaarIdentityId === 'string' && doc.aadhaarIdentityId.length > 0;
const hasToken = typeof doc?.aadhaarTokenId === 'string' && doc.aadhaarTokenId.length > 0;
const looksMasked = /^XXXX-XXXX-\d{4}$/.test(mask);
if (!hasIdentity) t.missingIdentityId += 1;
if (looksMasked && hasToken && hasIdentity) t.fullyTokenized += 1;
for (const key of Object.keys(doc || {})) {
if (/^aadhaa?r/i.test(key) && !KNOWN_AADHAAR_FIELDS.has(key)) {
t.unexpectedFields[key] = (t.unexpectedFields[key] || 0) + 1;
}
}
}

function printReport(t: AuditTally, source: string): void {
console.log('──────────────────────────────────────────────');
console.log(`Aadhaar-at-rest audit — source: ${source}`);
console.log('──────────────────────────────────────────────');
console.log(`total student documents scanned: ${t.total}`);
console.log(`raw Aadhaar (aadharMasked == 12 digits): ${t.rawMaskExact}`);
console.log(`raw Aadhaar embedded (12-digit run): ${t.rawMaskEmbedded}`);
console.log(`missing aadhaarIdentityId: ${t.missingIdentityId}`);
console.log(`fully tokenized (mask+token+identity): ${t.fullyTokenized}`);
const unexpectedKeys = Object.keys(t.unexpectedFields);
if (unexpectedKeys.length === 0) {
console.log('unexpected Aadhaar-related fields: none');
} else {
console.log('unexpected Aadhaar-related fields:');
for (const key of unexpectedKeys) console.log(` - ${key}: ${t.unexpectedFields[key]} document(s)`);
}
console.log('──────────────────────────────────────────────');
console.log('This is a READ-ONLY audit. No data was modified.');
}

async function main(): Promise<void> {
await connectDB(); // repository convention (see reseed.ts); no-op without MONGODB_URI
await dbStore.init(); // wires the Mongo handle or loads the file store

const t = newTally();
const db = dbStore.getDb();
if (db) {
// Mongo path: cursor with a narrow projection — only Aadhaar-relevant
// fields are pulled across the wire, and only counters are kept.
const projection = { aadharMasked: 1, aadhaarTokenId: 1, aadhaarIdentityId: 1 };
const cursor = db.collection('students').find({}, { projection: projection as any });
let scannedViaCursor = 0;
while (await cursor.hasNext()) {
tallyDoc(t, await cursor.next());
scannedViaCursor += 1;
}
await cursor.close();
printReport(t, `MongoDB collection "students" (${scannedViaCursor} docs)`);
} else {
// Local file-fallback store.
const students = await dbStore.getStudents();
students.forEach(s => tallyDoc(t, s));
printReport(t, 'local file store (data/db.json)');
}
process.exit(0);
}

main().catch(err => {
console.error('[aadhaar-audit] failed:', err?.message || err);
process.exit(1);
});
Loading