aadhaar-react-scanner 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Aadhaar React Scanner
2
+
3
+ A pure Javascript, zero-backend, client-side library to decode Aadhaar Secure QR codes, extract biometric JP2 photos, cryptographically verify UIDAI signatures, and render a beautiful 3D virtual identity card.
4
+
5
+ ## Features
6
+ - **Local Decoding**: Decodes the massive Aadhaar integer strings into raw demographic data completely offline.
7
+ - **JP2 Photo Extraction**: The only JS library that natively decodes the embedded JPEG 2000 biometric photo into a web-friendly PNG entirely in the browser using HTML5 Canvas.
8
+ - **UIDAI Cryptographic Verification**: Uses the Web Crypto API to automatically verify the 256-byte RSA signature against the historical database of official UIDAI public keys.
9
+ - **Backward Compatible**: Supports parsing older XML-format Aadhaar QR codes out of the box.
10
+ - **3D React UI**: Comes with a hyper-realistic, glare-enabled, interactive 3D Aadhaar Card component.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install aadhaar-react-scanner
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### 1. Decoding the QR Data
21
+ Pass the raw QR string (either the massive integer or the old XML string) directly into the decoder.
22
+
23
+ ```typescript
24
+ import { decodeAadhaarQR } from 'aadhaar-react-scanner';
25
+
26
+ const rawQrString = "697941..."; // Scanned from the QR code
27
+
28
+ const verify = async () => {
29
+ const result = await decodeAadhaarQR(rawQrString);
30
+
31
+ if (result.success) {
32
+ console.log("Resident Name:", result.data.name);
33
+ console.log("Valid Signature?", result.data.signature_valid);
34
+ } else {
35
+ console.error("Failed to decode:", result.error);
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### 2. Displaying the 3D Card
41
+ If the decode is successful, pass the data directly into the React component to render a stunning virtual card.
42
+
43
+ ```tsx
44
+ import { VirtualAadhaarCard } from 'aadhaar-react-scanner';
45
+
46
+ function MyVerificationPage() {
47
+ // Assuming 'result.data' from decodeAadhaarQR
48
+ return <VirtualAadhaarCard data={result.data} />;
49
+ }
50
+ ```
51
+
52
+ ## Security
53
+ This library is entirely client-side. No data is ever transmitted to any external servers. The cryptographic signature verification ensures that any manipulated QR data is immediately flagged as a `Signature Mismatch`.
54
+ # aadharreader
@@ -0,0 +1,30 @@
1
+ interface AadhaarData {
2
+ email_mobile_indicator?: string;
3
+ reference_id?: string;
4
+ name?: string;
5
+ dob?: string;
6
+ gender?: string;
7
+ care_of?: string;
8
+ district?: string;
9
+ landmark?: string;
10
+ house?: string;
11
+ location?: string;
12
+ pincode?: string;
13
+ post_office?: string;
14
+ state?: string;
15
+ street?: string;
16
+ sub_district?: string;
17
+ vtc?: string;
18
+ aadhaar_last4?: string;
19
+ photo_base64?: string;
20
+ photo_mime?: string;
21
+ signature_valid?: boolean;
22
+ }
23
+ interface DecodeResult {
24
+ success: boolean;
25
+ data?: AadhaarData;
26
+ error?: string;
27
+ }
28
+ declare function decodeAadhaarQR(qrString: string): Promise<DecodeResult>;
29
+
30
+ export { type AadhaarData, type DecodeResult, decodeAadhaarQR };
@@ -0,0 +1,30 @@
1
+ interface AadhaarData {
2
+ email_mobile_indicator?: string;
3
+ reference_id?: string;
4
+ name?: string;
5
+ dob?: string;
6
+ gender?: string;
7
+ care_of?: string;
8
+ district?: string;
9
+ landmark?: string;
10
+ house?: string;
11
+ location?: string;
12
+ pincode?: string;
13
+ post_office?: string;
14
+ state?: string;
15
+ street?: string;
16
+ sub_district?: string;
17
+ vtc?: string;
18
+ aadhaar_last4?: string;
19
+ photo_base64?: string;
20
+ photo_mime?: string;
21
+ signature_valid?: boolean;
22
+ }
23
+ interface DecodeResult {
24
+ success: boolean;
25
+ data?: AadhaarData;
26
+ error?: string;
27
+ }
28
+ declare function decodeAadhaarQR(qrString: string): Promise<DecodeResult>;
29
+
30
+ export { type AadhaarData, type DecodeResult, decodeAadhaarQR };
package/dist/index.js ADDED
@@ -0,0 +1,294 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/index.ts
30
+ var index_exports = {};
31
+ __export(index_exports, {
32
+ decodeAadhaarQR: () => decodeAadhaarQR
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/decoder.ts
37
+ var pako = __toESM(require("pako"));
38
+ var import_jpeg2000 = require("jpeg2000");
39
+ var import_buffer = require("buffer");
40
+ function bigIntToBytes(bigIntStr) {
41
+ let hex = BigInt(bigIntStr.trim()).toString(16);
42
+ if (hex.length % 2 !== 0) {
43
+ hex = "0" + hex;
44
+ }
45
+ const len = hex.length / 2;
46
+ const u8 = new Uint8Array(len);
47
+ for (let i = 0; i < len; i++) {
48
+ u8[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
49
+ }
50
+ return u8;
51
+ }
52
+ function jp2ToDataURL(jp2Bytes) {
53
+ const jpx = new import_jpeg2000.JpxImage();
54
+ const buf = import_buffer.Buffer.from(jp2Bytes);
55
+ jpx.parse(buf);
56
+ const width = jpx.width;
57
+ const height = jpx.height;
58
+ const componentsCount = jpx.componentsCount;
59
+ const tiles = jpx.tiles;
60
+ if (!tiles || tiles.length === 0) throw new Error("No tiles found in JP2");
61
+ const items = tiles[0].items;
62
+ const canvas = document.createElement("canvas");
63
+ canvas.width = width;
64
+ canvas.height = height;
65
+ const ctx = canvas.getContext("2d");
66
+ if (!ctx) throw new Error("Canvas context not available");
67
+ const imgData = ctx.createImageData(width, height);
68
+ const data = imgData.data;
69
+ if (componentsCount === 3 || componentsCount === 4) {
70
+ const step = componentsCount;
71
+ for (let i = 0, j = 0; i < items.length; i += step, j += 4) {
72
+ data[j] = items[i];
73
+ data[j + 1] = items[i + 1];
74
+ data[j + 2] = items[i + 2];
75
+ data[j + 3] = componentsCount === 4 ? items[i + 3] : 255;
76
+ }
77
+ } else if (componentsCount === 1) {
78
+ for (let i = 0, j = 0; i < items.length; i++, j += 4) {
79
+ const val = items[i];
80
+ data[j] = val;
81
+ data[j + 1] = val;
82
+ data[j + 2] = val;
83
+ data[j + 3] = 255;
84
+ }
85
+ } else {
86
+ throw new Error("Unsupported components count: " + componentsCount);
87
+ }
88
+ ctx.putImageData(imgData, 0, 0);
89
+ return canvas.toDataURL("image/png");
90
+ }
91
+ var UIDAI_PUBLIC_KEYS = [
92
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh1+zYnvbcEm0Yz73s5u42odpUJMr9wv5bVw7sOE5nFNbrB+U++5I0f8cL2HoHnJOkwvLZzrD0jG/vxAKi6vii/gjEzUEgrkdIHxMP3D6GJs0MSQHiEXvIGOwPIH3BLtBOc3m28NVNT6Q9iq0gUwuxnlhV38UdNhCllqNYhWmAMPJkImgaKrRZvY2pWNs6gd+PlAF/9SO69x3+1meA8kPk2ZvQanZlx9tfaExeOe9or3NQiKy2+UbtXrpcoAfYbbWi1OUzXi5bJdhbGp239c1fX6UKyUM5IUMY+m3I7wu2WQ7lmeO2n/vwzQz/PKHXPWYu3bydWMLdCi07vOQBqzCKwIDAQAB",
93
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv1DSK9/qrW8RX1vDZMsE8xiiyJlj+6xxDtu+nSZDW9C/iajSqJ2QgLvRgweTw4suzzxZQseOE+kbqlbesNHc0lQjt9T+CGYrUTCbMI/a3zZbr3vPxz3VlN7iqr8U6ISUN53x+6qAc4Z/Pc66IqJA6zXBPKFZiHHMmi00eM14HgNWrLEkYHE5geBmBgEevznskS4Q+sJVX+4seJ/zadc35O4G6gvWZatlsB5STGSdes4TqF1k0FV4a0CF7vAzpUA4EtQohl6dnKWpfWYAJUxbSrH1OCLFBn1ABe9Yw5iZkIMFYauhyFzP16XCiG91TPoORIJ8ssIR9uf21o6rD82OJwIDAQAB",
94
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmMIJKj28JcTN1B72p2/pgzDCoguhs/rbIXgN/ybNNh0NVOrZV2KllrmT5VOYlMrABpvIp7JU/n6hma3/O14n7nvngJ/y3colh8rk7msDwVAO7ZuVD+GCzfaYPLLkUS+wqH7M7FOHIn/pyJo1Rkxm98lO3dyox5RuLG2Uqm7JfVIomm0t7QKJoM5rf8JNvPXdwsxN89eWlT2Bf7BF//G3FKiF7ZHfvIyyqte/3orRRG/M80QqLrDP1RIeOa53ZTgILXcyQOb2yZOqNH3iN2uSKRsusNO17To5FOb2J9Hd5wIMuDv3zw4MWTrKAWuTYon90QSeGRKv1d5AQNRt0x5dSwIDAQAB",
95
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv0HjbFpvu/kR+gTI2+svGNmW4eZHhTVBG/N+byaq3GH0SDM+jO5RW4BbXNzaSKc0I5mIyN1vQf2KmNV/3Xai6MokiiZrBRfM8a497zCMteHTAzSP1L0DmohUuBQh/s1hfqRIIWpfEu7noW2G8toK0ZOQR1E0FtinWNtqEeuxlNEKgfxkN4/vRzgvGFw+PPcoG5uMdcd7/DjDE1i20zmT+55DgIBrneCwrW7nIM0Md3BPOTV8iBwzjdVcdDHhMtSpi9UKUHw80sDRZp7ygB4Z0QmhSxCMCg9g7KPHYY+PVRC2sFreZBC6rtmIL+HMUPciRCCqMZLx3f6xRSD97lZr/wIDAQAB",
96
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAonIsDl8t5bpwftk/A27CsfC5VZMjkPrMDwvL8gyAoVwIi0iGhmty6yWrC/VaL+Brae29XMg7dMdwnbIUHmwHxovN+FnT2vfz/O0kHQcgVdwVSIR0tFwsmC+pVKpSqm//skgYYcZQhdhLZBWOn0PZ81ymm0jOkwBSIQKkyuCTv/1HSwjTLR0EBvaH9+Vb0iaiOEv1ikHDhMOXTxx8URWBnJJt463z7LuZBMSG8fXVMDl3vqY1hDZzKbXBaK/clRIXMff0jUOvfPMfabHju+eUnceosQwL3eurq96+oHahz4FmrfBqikHe3xQ7/4NdvSvVuwth0kcsI0ptRBG8m1NglQIDAQAB"
97
+ ];
98
+ async function verifySignature(signedData, signature) {
99
+ try {
100
+ for (const pem of UIDAI_PUBLIC_KEYS) {
101
+ const binaryDerString = window.atob(pem);
102
+ const binaryDer = new Uint8Array(binaryDerString.length);
103
+ for (let i = 0; i < binaryDerString.length; i++) {
104
+ binaryDer[i] = binaryDerString.charCodeAt(i);
105
+ }
106
+ try {
107
+ const key = await window.crypto.subtle.importKey(
108
+ "spki",
109
+ binaryDer.buffer,
110
+ {
111
+ name: "RSASSA-PKCS1-v1_5",
112
+ hash: "SHA-256"
113
+ },
114
+ false,
115
+ ["verify"]
116
+ );
117
+ const isValid = await window.crypto.subtle.verify(
118
+ "RSASSA-PKCS1-v1_5",
119
+ key,
120
+ signature,
121
+ signedData
122
+ );
123
+ if (isValid) return true;
124
+ } catch (err) {
125
+ }
126
+ }
127
+ return false;
128
+ } catch (e) {
129
+ console.error("Signature verification failed:", e);
130
+ return false;
131
+ }
132
+ }
133
+ async function decodeAadhaarQR(qrString) {
134
+ try {
135
+ const rawQr = qrString.trim();
136
+ if (rawQr.startsWith("<")) {
137
+ const parser = new DOMParser();
138
+ const xmlDoc = parser.parseFromString(rawQr, "text/xml");
139
+ const parseError = xmlDoc.getElementsByTagName("parsererror");
140
+ if (parseError.length > 0) {
141
+ return { success: false, error: "Failed to parse XML data" };
142
+ }
143
+ const root = xmlDoc.documentElement;
144
+ const result2 = {};
145
+ const fieldMap = {
146
+ "uid": "aadhaar_last4",
147
+ // We'll extract last 4 below
148
+ "name": "name",
149
+ "gender": "gender",
150
+ "yob": "dob",
151
+ // Fallback if dob isn't present
152
+ "dob": "dob",
153
+ "co": "care_of",
154
+ "house": "house",
155
+ "street": "street",
156
+ "lm": "landmark",
157
+ "loc": "location",
158
+ "vtc": "vtc",
159
+ "po": "post_office",
160
+ "dist": "district",
161
+ "subdist": "sub_district",
162
+ "state": "state",
163
+ "pc": "pincode"
164
+ };
165
+ for (const [xmlKey, resKey] of Object.entries(fieldMap)) {
166
+ const val = root.getAttribute(xmlKey);
167
+ if (val) {
168
+ result2[resKey] = val;
169
+ }
170
+ }
171
+ if (root.getAttribute("uid") && root.getAttribute("uid").length >= 4) {
172
+ result2.aadhaar_last4 = root.getAttribute("uid").slice(-4);
173
+ }
174
+ return { success: true, data: result2 };
175
+ }
176
+ const isSecure = rawQr.match(/^\d{100,}$/);
177
+ if (!isSecure) {
178
+ return { success: false, error: "Invalid QR string format." };
179
+ }
180
+ const rawBytes = bigIntToBytes(rawQr);
181
+ let decompressed = null;
182
+ const windowBitsOptions = [15, -15, 31, 47];
183
+ for (let pad = 0; pad < 4; pad++) {
184
+ let paddedBytes = rawBytes;
185
+ if (pad > 0) {
186
+ paddedBytes = new Uint8Array(rawBytes.length + pad);
187
+ paddedBytes.set(rawBytes, pad);
188
+ }
189
+ for (const wbits of windowBitsOptions) {
190
+ try {
191
+ decompressed = pako.inflate(paddedBytes, { windowBits: wbits });
192
+ break;
193
+ } catch (e) {
194
+ }
195
+ }
196
+ if (decompressed) break;
197
+ }
198
+ if (!decompressed) {
199
+ return { success: false, error: "Failed to decompress Aadhaar data (Zlib error)" };
200
+ }
201
+ const signedData = decompressed.slice(0, -256);
202
+ const signature = decompressed.slice(-256);
203
+ const data = signedData;
204
+ const DELIMITER = 255;
205
+ let parts = [];
206
+ let start = 0;
207
+ let dob_idx = -1;
208
+ for (let i = 0; i < data.length; i++) {
209
+ if (data[i] === DELIMITER) {
210
+ const valBytes = data.slice(start, i);
211
+ const val = Array.from(valBytes).map((b) => String.fromCharCode(b)).join("");
212
+ parts.push(val);
213
+ start = i + 1;
214
+ if (dob_idx === -1 && ["M", "F", "T"].includes(val) && parts.length > 3) {
215
+ dob_idx = parts.length - 2;
216
+ }
217
+ if (dob_idx !== -1 && parts.length === dob_idx + 13) {
218
+ start = i + 1;
219
+ break;
220
+ }
221
+ }
222
+ }
223
+ const result = {};
224
+ result.email_mobile_indicator = parts[0];
225
+ if (dob_idx !== -1) {
226
+ const getP = (i) => i < parts.length ? parts[i].trim() : "";
227
+ result.reference_id = getP(dob_idx - 2);
228
+ result.name = getP(dob_idx - 1);
229
+ result.dob = getP(dob_idx);
230
+ result.gender = getP(dob_idx + 1);
231
+ result.care_of = getP(dob_idx + 2);
232
+ result.district = getP(dob_idx + 3);
233
+ result.landmark = getP(dob_idx + 4);
234
+ result.house = getP(dob_idx + 5);
235
+ result.location = getP(dob_idx + 6);
236
+ result.pincode = getP(dob_idx + 7);
237
+ result.post_office = getP(dob_idx + 8);
238
+ result.state = getP(dob_idx + 9);
239
+ result.street = getP(dob_idx + 10);
240
+ result.sub_district = getP(dob_idx + 11);
241
+ result.vtc = getP(dob_idx + 12);
242
+ }
243
+ if (result.reference_id && result.reference_id.length >= 4) {
244
+ result.aadhaar_last4 = result.reference_id.substring(0, 4);
245
+ }
246
+ if (start < data.length) {
247
+ let tail = data.length;
248
+ const ind = parseInt(parts[0], 10) || 0;
249
+ if (ind === 3) tail -= 64;
250
+ else if (ind === 1 || ind === 2) tail -= 32;
251
+ let photoBytes = data.slice(start, tail);
252
+ let photoStart = -1;
253
+ for (let j = 0; j < photoBytes.length - 3; j++) {
254
+ if (photoBytes[j] === 255 && photoBytes[j + 1] === 79 && photoBytes[j + 2] === 255 && photoBytes[j + 3] === 81) {
255
+ photoStart = j;
256
+ break;
257
+ }
258
+ }
259
+ if (photoStart === -1) {
260
+ for (let j = 0; j < photoBytes.length - 7; j++) {
261
+ if (photoBytes[j] === 0 && photoBytes[j + 1] === 0 && photoBytes[j + 2] === 0 && photoBytes[j + 3] === 12 && photoBytes[j + 4] === 106 && photoBytes[j + 5] === 80 && photoBytes[j + 6] === 32 && photoBytes[j + 7] === 32) {
262
+ photoStart = j;
263
+ break;
264
+ }
265
+ }
266
+ }
267
+ if (photoStart !== -1) {
268
+ photoBytes = photoBytes.slice(photoStart);
269
+ }
270
+ try {
271
+ const dataUrl = jp2ToDataURL(photoBytes);
272
+ result.photo_base64 = dataUrl.split(",")[1];
273
+ result.photo_mime = "image/png";
274
+ } catch (err) {
275
+ console.error("Canvas JP2 decoding failed:", err);
276
+ const CHUNK_SIZE = 32768;
277
+ let c = [];
278
+ for (let i = 0; i < photoBytes.length; i += CHUNK_SIZE) {
279
+ c.push(String.fromCharCode.apply(null, Array.from(photoBytes.subarray(i, i + CHUNK_SIZE))));
280
+ }
281
+ result.photo_base64 = btoa(c.join(""));
282
+ result.photo_mime = "image/jp2";
283
+ }
284
+ }
285
+ result.signature_valid = await verifySignature(signedData, signature);
286
+ return { success: true, data: result };
287
+ } catch (error) {
288
+ return { success: false, error: error.message || "Unknown error occurred during parsing" };
289
+ }
290
+ }
291
+ // Annotate the CommonJS export names for ESM import in node:
292
+ 0 && (module.exports = {
293
+ decodeAadhaarQR
294
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,258 @@
1
+ // src/decoder.ts
2
+ import * as pako from "pako";
3
+ import { JpxImage } from "jpeg2000";
4
+ import { Buffer } from "buffer";
5
+ function bigIntToBytes(bigIntStr) {
6
+ let hex = BigInt(bigIntStr.trim()).toString(16);
7
+ if (hex.length % 2 !== 0) {
8
+ hex = "0" + hex;
9
+ }
10
+ const len = hex.length / 2;
11
+ const u8 = new Uint8Array(len);
12
+ for (let i = 0; i < len; i++) {
13
+ u8[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
14
+ }
15
+ return u8;
16
+ }
17
+ function jp2ToDataURL(jp2Bytes) {
18
+ const jpx = new JpxImage();
19
+ const buf = Buffer.from(jp2Bytes);
20
+ jpx.parse(buf);
21
+ const width = jpx.width;
22
+ const height = jpx.height;
23
+ const componentsCount = jpx.componentsCount;
24
+ const tiles = jpx.tiles;
25
+ if (!tiles || tiles.length === 0) throw new Error("No tiles found in JP2");
26
+ const items = tiles[0].items;
27
+ const canvas = document.createElement("canvas");
28
+ canvas.width = width;
29
+ canvas.height = height;
30
+ const ctx = canvas.getContext("2d");
31
+ if (!ctx) throw new Error("Canvas context not available");
32
+ const imgData = ctx.createImageData(width, height);
33
+ const data = imgData.data;
34
+ if (componentsCount === 3 || componentsCount === 4) {
35
+ const step = componentsCount;
36
+ for (let i = 0, j = 0; i < items.length; i += step, j += 4) {
37
+ data[j] = items[i];
38
+ data[j + 1] = items[i + 1];
39
+ data[j + 2] = items[i + 2];
40
+ data[j + 3] = componentsCount === 4 ? items[i + 3] : 255;
41
+ }
42
+ } else if (componentsCount === 1) {
43
+ for (let i = 0, j = 0; i < items.length; i++, j += 4) {
44
+ const val = items[i];
45
+ data[j] = val;
46
+ data[j + 1] = val;
47
+ data[j + 2] = val;
48
+ data[j + 3] = 255;
49
+ }
50
+ } else {
51
+ throw new Error("Unsupported components count: " + componentsCount);
52
+ }
53
+ ctx.putImageData(imgData, 0, 0);
54
+ return canvas.toDataURL("image/png");
55
+ }
56
+ var UIDAI_PUBLIC_KEYS = [
57
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh1+zYnvbcEm0Yz73s5u42odpUJMr9wv5bVw7sOE5nFNbrB+U++5I0f8cL2HoHnJOkwvLZzrD0jG/vxAKi6vii/gjEzUEgrkdIHxMP3D6GJs0MSQHiEXvIGOwPIH3BLtBOc3m28NVNT6Q9iq0gUwuxnlhV38UdNhCllqNYhWmAMPJkImgaKrRZvY2pWNs6gd+PlAF/9SO69x3+1meA8kPk2ZvQanZlx9tfaExeOe9or3NQiKy2+UbtXrpcoAfYbbWi1OUzXi5bJdhbGp239c1fX6UKyUM5IUMY+m3I7wu2WQ7lmeO2n/vwzQz/PKHXPWYu3bydWMLdCi07vOQBqzCKwIDAQAB",
58
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv1DSK9/qrW8RX1vDZMsE8xiiyJlj+6xxDtu+nSZDW9C/iajSqJ2QgLvRgweTw4suzzxZQseOE+kbqlbesNHc0lQjt9T+CGYrUTCbMI/a3zZbr3vPxz3VlN7iqr8U6ISUN53x+6qAc4Z/Pc66IqJA6zXBPKFZiHHMmi00eM14HgNWrLEkYHE5geBmBgEevznskS4Q+sJVX+4seJ/zadc35O4G6gvWZatlsB5STGSdes4TqF1k0FV4a0CF7vAzpUA4EtQohl6dnKWpfWYAJUxbSrH1OCLFBn1ABe9Yw5iZkIMFYauhyFzP16XCiG91TPoORIJ8ssIR9uf21o6rD82OJwIDAQAB",
59
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmMIJKj28JcTN1B72p2/pgzDCoguhs/rbIXgN/ybNNh0NVOrZV2KllrmT5VOYlMrABpvIp7JU/n6hma3/O14n7nvngJ/y3colh8rk7msDwVAO7ZuVD+GCzfaYPLLkUS+wqH7M7FOHIn/pyJo1Rkxm98lO3dyox5RuLG2Uqm7JfVIomm0t7QKJoM5rf8JNvPXdwsxN89eWlT2Bf7BF//G3FKiF7ZHfvIyyqte/3orRRG/M80QqLrDP1RIeOa53ZTgILXcyQOb2yZOqNH3iN2uSKRsusNO17To5FOb2J9Hd5wIMuDv3zw4MWTrKAWuTYon90QSeGRKv1d5AQNRt0x5dSwIDAQAB",
60
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv0HjbFpvu/kR+gTI2+svGNmW4eZHhTVBG/N+byaq3GH0SDM+jO5RW4BbXNzaSKc0I5mIyN1vQf2KmNV/3Xai6MokiiZrBRfM8a497zCMteHTAzSP1L0DmohUuBQh/s1hfqRIIWpfEu7noW2G8toK0ZOQR1E0FtinWNtqEeuxlNEKgfxkN4/vRzgvGFw+PPcoG5uMdcd7/DjDE1i20zmT+55DgIBrneCwrW7nIM0Md3BPOTV8iBwzjdVcdDHhMtSpi9UKUHw80sDRZp7ygB4Z0QmhSxCMCg9g7KPHYY+PVRC2sFreZBC6rtmIL+HMUPciRCCqMZLx3f6xRSD97lZr/wIDAQAB",
61
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAonIsDl8t5bpwftk/A27CsfC5VZMjkPrMDwvL8gyAoVwIi0iGhmty6yWrC/VaL+Brae29XMg7dMdwnbIUHmwHxovN+FnT2vfz/O0kHQcgVdwVSIR0tFwsmC+pVKpSqm//skgYYcZQhdhLZBWOn0PZ81ymm0jOkwBSIQKkyuCTv/1HSwjTLR0EBvaH9+Vb0iaiOEv1ikHDhMOXTxx8URWBnJJt463z7LuZBMSG8fXVMDl3vqY1hDZzKbXBaK/clRIXMff0jUOvfPMfabHju+eUnceosQwL3eurq96+oHahz4FmrfBqikHe3xQ7/4NdvSvVuwth0kcsI0ptRBG8m1NglQIDAQAB"
62
+ ];
63
+ async function verifySignature(signedData, signature) {
64
+ try {
65
+ for (const pem of UIDAI_PUBLIC_KEYS) {
66
+ const binaryDerString = window.atob(pem);
67
+ const binaryDer = new Uint8Array(binaryDerString.length);
68
+ for (let i = 0; i < binaryDerString.length; i++) {
69
+ binaryDer[i] = binaryDerString.charCodeAt(i);
70
+ }
71
+ try {
72
+ const key = await window.crypto.subtle.importKey(
73
+ "spki",
74
+ binaryDer.buffer,
75
+ {
76
+ name: "RSASSA-PKCS1-v1_5",
77
+ hash: "SHA-256"
78
+ },
79
+ false,
80
+ ["verify"]
81
+ );
82
+ const isValid = await window.crypto.subtle.verify(
83
+ "RSASSA-PKCS1-v1_5",
84
+ key,
85
+ signature,
86
+ signedData
87
+ );
88
+ if (isValid) return true;
89
+ } catch (err) {
90
+ }
91
+ }
92
+ return false;
93
+ } catch (e) {
94
+ console.error("Signature verification failed:", e);
95
+ return false;
96
+ }
97
+ }
98
+ async function decodeAadhaarQR(qrString) {
99
+ try {
100
+ const rawQr = qrString.trim();
101
+ if (rawQr.startsWith("<")) {
102
+ const parser = new DOMParser();
103
+ const xmlDoc = parser.parseFromString(rawQr, "text/xml");
104
+ const parseError = xmlDoc.getElementsByTagName("parsererror");
105
+ if (parseError.length > 0) {
106
+ return { success: false, error: "Failed to parse XML data" };
107
+ }
108
+ const root = xmlDoc.documentElement;
109
+ const result2 = {};
110
+ const fieldMap = {
111
+ "uid": "aadhaar_last4",
112
+ // We'll extract last 4 below
113
+ "name": "name",
114
+ "gender": "gender",
115
+ "yob": "dob",
116
+ // Fallback if dob isn't present
117
+ "dob": "dob",
118
+ "co": "care_of",
119
+ "house": "house",
120
+ "street": "street",
121
+ "lm": "landmark",
122
+ "loc": "location",
123
+ "vtc": "vtc",
124
+ "po": "post_office",
125
+ "dist": "district",
126
+ "subdist": "sub_district",
127
+ "state": "state",
128
+ "pc": "pincode"
129
+ };
130
+ for (const [xmlKey, resKey] of Object.entries(fieldMap)) {
131
+ const val = root.getAttribute(xmlKey);
132
+ if (val) {
133
+ result2[resKey] = val;
134
+ }
135
+ }
136
+ if (root.getAttribute("uid") && root.getAttribute("uid").length >= 4) {
137
+ result2.aadhaar_last4 = root.getAttribute("uid").slice(-4);
138
+ }
139
+ return { success: true, data: result2 };
140
+ }
141
+ const isSecure = rawQr.match(/^\d{100,}$/);
142
+ if (!isSecure) {
143
+ return { success: false, error: "Invalid QR string format." };
144
+ }
145
+ const rawBytes = bigIntToBytes(rawQr);
146
+ let decompressed = null;
147
+ const windowBitsOptions = [15, -15, 31, 47];
148
+ for (let pad = 0; pad < 4; pad++) {
149
+ let paddedBytes = rawBytes;
150
+ if (pad > 0) {
151
+ paddedBytes = new Uint8Array(rawBytes.length + pad);
152
+ paddedBytes.set(rawBytes, pad);
153
+ }
154
+ for (const wbits of windowBitsOptions) {
155
+ try {
156
+ decompressed = pako.inflate(paddedBytes, { windowBits: wbits });
157
+ break;
158
+ } catch (e) {
159
+ }
160
+ }
161
+ if (decompressed) break;
162
+ }
163
+ if (!decompressed) {
164
+ return { success: false, error: "Failed to decompress Aadhaar data (Zlib error)" };
165
+ }
166
+ const signedData = decompressed.slice(0, -256);
167
+ const signature = decompressed.slice(-256);
168
+ const data = signedData;
169
+ const DELIMITER = 255;
170
+ let parts = [];
171
+ let start = 0;
172
+ let dob_idx = -1;
173
+ for (let i = 0; i < data.length; i++) {
174
+ if (data[i] === DELIMITER) {
175
+ const valBytes = data.slice(start, i);
176
+ const val = Array.from(valBytes).map((b) => String.fromCharCode(b)).join("");
177
+ parts.push(val);
178
+ start = i + 1;
179
+ if (dob_idx === -1 && ["M", "F", "T"].includes(val) && parts.length > 3) {
180
+ dob_idx = parts.length - 2;
181
+ }
182
+ if (dob_idx !== -1 && parts.length === dob_idx + 13) {
183
+ start = i + 1;
184
+ break;
185
+ }
186
+ }
187
+ }
188
+ const result = {};
189
+ result.email_mobile_indicator = parts[0];
190
+ if (dob_idx !== -1) {
191
+ const getP = (i) => i < parts.length ? parts[i].trim() : "";
192
+ result.reference_id = getP(dob_idx - 2);
193
+ result.name = getP(dob_idx - 1);
194
+ result.dob = getP(dob_idx);
195
+ result.gender = getP(dob_idx + 1);
196
+ result.care_of = getP(dob_idx + 2);
197
+ result.district = getP(dob_idx + 3);
198
+ result.landmark = getP(dob_idx + 4);
199
+ result.house = getP(dob_idx + 5);
200
+ result.location = getP(dob_idx + 6);
201
+ result.pincode = getP(dob_idx + 7);
202
+ result.post_office = getP(dob_idx + 8);
203
+ result.state = getP(dob_idx + 9);
204
+ result.street = getP(dob_idx + 10);
205
+ result.sub_district = getP(dob_idx + 11);
206
+ result.vtc = getP(dob_idx + 12);
207
+ }
208
+ if (result.reference_id && result.reference_id.length >= 4) {
209
+ result.aadhaar_last4 = result.reference_id.substring(0, 4);
210
+ }
211
+ if (start < data.length) {
212
+ let tail = data.length;
213
+ const ind = parseInt(parts[0], 10) || 0;
214
+ if (ind === 3) tail -= 64;
215
+ else if (ind === 1 || ind === 2) tail -= 32;
216
+ let photoBytes = data.slice(start, tail);
217
+ let photoStart = -1;
218
+ for (let j = 0; j < photoBytes.length - 3; j++) {
219
+ if (photoBytes[j] === 255 && photoBytes[j + 1] === 79 && photoBytes[j + 2] === 255 && photoBytes[j + 3] === 81) {
220
+ photoStart = j;
221
+ break;
222
+ }
223
+ }
224
+ if (photoStart === -1) {
225
+ for (let j = 0; j < photoBytes.length - 7; j++) {
226
+ if (photoBytes[j] === 0 && photoBytes[j + 1] === 0 && photoBytes[j + 2] === 0 && photoBytes[j + 3] === 12 && photoBytes[j + 4] === 106 && photoBytes[j + 5] === 80 && photoBytes[j + 6] === 32 && photoBytes[j + 7] === 32) {
227
+ photoStart = j;
228
+ break;
229
+ }
230
+ }
231
+ }
232
+ if (photoStart !== -1) {
233
+ photoBytes = photoBytes.slice(photoStart);
234
+ }
235
+ try {
236
+ const dataUrl = jp2ToDataURL(photoBytes);
237
+ result.photo_base64 = dataUrl.split(",")[1];
238
+ result.photo_mime = "image/png";
239
+ } catch (err) {
240
+ console.error("Canvas JP2 decoding failed:", err);
241
+ const CHUNK_SIZE = 32768;
242
+ let c = [];
243
+ for (let i = 0; i < photoBytes.length; i += CHUNK_SIZE) {
244
+ c.push(String.fromCharCode.apply(null, Array.from(photoBytes.subarray(i, i + CHUNK_SIZE))));
245
+ }
246
+ result.photo_base64 = btoa(c.join(""));
247
+ result.photo_mime = "image/jp2";
248
+ }
249
+ }
250
+ result.signature_valid = await verifySignature(signedData, signature);
251
+ return { success: true, data: result };
252
+ } catch (error) {
253
+ return { success: false, error: error.message || "Unknown error occurred during parsing" };
254
+ }
255
+ }
256
+ export {
257
+ decodeAadhaarQR
258
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "aadhaar-react-scanner",
3
+ "version": "1.0.0",
4
+ "description": "Pure JS local Aadhaar QR parsing, JP2 decoding, and 3D React UI",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsup",
10
+ "dev": "tsup --watch"
11
+ },
12
+ "keywords": [
13
+ "aadhaar",
14
+ "qr",
15
+ "scanner",
16
+ "react",
17
+ "uidai",
18
+ "jp2"
19
+ ],
20
+ "author": "",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "buffer": "^6.0.3",
24
+ "jpeg2000": "^1.1.0",
25
+ "pako": "^2.1.0"
26
+ },
27
+ "peerDependencies": {
28
+ "lucide-react": "^0.462.0",
29
+ "react": "^18.0.0",
30
+ "react-dom": "^18.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/react": "^18.0.0",
34
+ "@types/react-dom": "^18.0.0",
35
+ "tsup": "^8.0.2",
36
+ "typescript": "^5.0.0"
37
+ }
38
+ }
@@ -0,0 +1,133 @@
1
+ import React, { useState, useRef } from "react";
2
+ import { ScanFace } from "lucide-react";
3
+ import { AadhaarData } from "./decoder";
4
+
5
+ // Import assets as base64/data URLs
6
+ import emblemSrc from "./assets/emblem.png";
7
+ import cardBgSrc from "./assets/card-bg.png";
8
+
9
+ export const VirtualAadhaarCard = ({ data }: { data: AadhaarData }) => {
10
+ const cardRef = useRef<HTMLDivElement>(null);
11
+ const [transform, setTransform] = useState("");
12
+ const [glare, setGlare] = useState("transparent");
13
+
14
+ const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
15
+ if (!cardRef.current) return;
16
+ const rect = cardRef.current.getBoundingClientRect();
17
+ const x = e.clientX - rect.left;
18
+ const y = e.clientY - rect.top;
19
+ const centerX = rect.width / 2;
20
+ const centerY = rect.height / 2;
21
+
22
+ // Rotate max 10 degrees
23
+ const rotateX = ((y - centerY) / centerY) * -10;
24
+ const rotateY = ((x - centerX) / centerX) * 10;
25
+
26
+ setTransform(`perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale3d(1.02, 1.02, 1.02)`);
27
+
28
+ // Glare
29
+ const glareX = (x / rect.width) * 100;
30
+ const glareY = (y / rect.height) * 100;
31
+ setGlare(`radial-gradient(circle at ${glareX}% ${glareY}%, rgba(255,255,255,0.4) 0%, transparent 50%)`);
32
+ };
33
+
34
+ const handleMouseLeave = () => {
35
+ setTransform("perspective(1000px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)");
36
+ setGlare("transparent");
37
+ };
38
+
39
+ const displayAadhaar = data.aadhaar_last4 ? `XXXX XXXX ${data.aadhaar_last4}` : "XXXX XXXX XXXX";
40
+
41
+ return (
42
+ <div className="w-full flex justify-center py-4" style={{ perspective: "1000px" }}>
43
+ <div
44
+ ref={cardRef}
45
+ onMouseMove={handleMouseMove}
46
+ onMouseLeave={handleMouseLeave}
47
+ className="relative w-full max-w-[450px] aspect-[1.58] rounded-xl overflow-hidden shadow-2xl transition-transform duration-200 ease-out text-black border border-[#d2cbbb] select-none"
48
+ style={{
49
+ transform,
50
+ transformStyle: "preserve-3d",
51
+ backgroundImage: `url('${cardBgSrc}')`,
52
+ backgroundSize: "cover",
53
+ backgroundPosition: "center"
54
+ }}
55
+ >
56
+ {/* Glare Overlay */}
57
+ <div className="absolute inset-0 z-50 pointer-events-none transition-background duration-200 mix-blend-overlay" style={{ background: glare }} />
58
+
59
+ {/* Top Header Background */}
60
+ <div className="absolute top-0 left-0 w-full h-12 flex items-center justify-center pt-2">
61
+ <div className="flex items-center justify-center gap-2">
62
+ <img src={emblemSrc} alt="Emblem" className="w-10 h-10 object-contain mix-blend-multiply" />
63
+ <div className="flex flex-col items-center">
64
+ <p className="text-[11px] font-bold text-red-700 leading-none mb-[2px]">भारत सरकार</p>
65
+ <p className="text-[11px] font-bold text-green-700 leading-none">Government of India</p>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ {/* Content */}
71
+ <div className="absolute top-[3.5rem] left-0 w-full px-5 flex gap-5">
72
+ {/* Photo Box */}
73
+ <div className="w-[85px] h-[105px] bg-[#e1dfda] border-2 border-white shadow-sm flex flex-col items-center justify-center overflow-hidden shrink-0 relative">
74
+ {data.photo_base64 ? (
75
+ <img
76
+ src={`data:${data.photo_mime || 'image/jp2'};base64,${data.photo_base64}`}
77
+ alt="Resident"
78
+ className="w-full h-full object-cover"
79
+ onError={(e) => {
80
+ e.currentTarget.style.display = 'none';
81
+ const parent = e.currentTarget.parentElement;
82
+ if (parent && !parent.querySelector('.fallback-text')) {
83
+ const fallback = document.createElement('div');
84
+ fallback.className = 'fallback-text text-[8px] text-gray-500 font-bold text-center px-1 uppercase tracking-wider absolute inset-0 flex items-center justify-center bg-[#e1dfda]';
85
+ fallback.innerText = 'JP2 Format Unsupported';
86
+ parent.appendChild(fallback);
87
+ }
88
+ }}
89
+ />
90
+ ) : (
91
+ <>
92
+ <ScanFace className="w-10 h-10 text-gray-400 mb-1 opacity-50" />
93
+ <span className="text-[8px] text-gray-500 font-bold text-center px-1 uppercase tracking-wider">Photo Decode Skipped</span>
94
+ </>
95
+ )}
96
+ </div>
97
+
98
+ {/* Details */}
99
+ <div className="flex-1 flex flex-col pt-1 text-[12px] leading-tight font-sans tracking-wide">
100
+ <div className="mb-3">
101
+ <p className="font-extrabold text-[15px] uppercase text-gray-900">{data.name || "N/A"}</p>
102
+ </div>
103
+
104
+ <div className="flex gap-6 mb-3">
105
+ <div>
106
+ <p className="text-gray-500 font-semibold mb-0.5">जन्म तिथि / DOB</p>
107
+ <p className="font-bold text-gray-900">{data.dob || "N/A"}</p>
108
+ </div>
109
+ </div>
110
+
111
+ <div className="flex gap-6">
112
+ <div>
113
+ <p className="text-gray-500 font-semibold mb-0.5">लिंग / Gender</p>
114
+ <p className="font-bold text-gray-900">{data.gender || "N/A"}</p>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ {/* Bottom Aadhaar Number & Red Bar */}
121
+ <div className="absolute bottom-0 left-0 w-full">
122
+ <div className="text-center mb-1.5">
123
+ <p className="text-[26px] font-extrabold tracking-[4px] font-mono text-gray-900">{displayAadhaar}</p>
124
+ </div>
125
+ <div className="h-1 w-full bg-[#E53E3E]" />
126
+ <div className="bg-[#E7F3E8] py-1.5 flex items-center justify-center border-t border-green-200">
127
+ <p className="text-[#E53E3E] font-bold text-[13px] tracking-wide">मेरा <span className="text-gray-900">आधार</span>, <span className="text-gray-900">मेरी पहचान</span></p>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ );
133
+ };
Binary file
Binary file
package/src/decoder.ts ADDED
@@ -0,0 +1,336 @@
1
+ import * as pako from 'pako';
2
+ // @ts-ignore
3
+ import { JpxImage } from 'jpeg2000';
4
+ import { Buffer } from 'buffer';
5
+
6
+ export interface AadhaarData {
7
+ email_mobile_indicator?: string;
8
+ reference_id?: string;
9
+ name?: string;
10
+ dob?: string;
11
+ gender?: string;
12
+ care_of?: string;
13
+ district?: string;
14
+ landmark?: string;
15
+ house?: string;
16
+ location?: string;
17
+ pincode?: string;
18
+ post_office?: string;
19
+ state?: string;
20
+ street?: string;
21
+ sub_district?: string;
22
+ vtc?: string;
23
+ aadhaar_last4?: string;
24
+ photo_base64?: string;
25
+ photo_mime?: string;
26
+ signature_valid?: boolean;
27
+ }
28
+
29
+ export interface DecodeResult {
30
+ success: boolean;
31
+ data?: AadhaarData;
32
+ error?: string;
33
+ }
34
+
35
+ function bigIntToBytes(bigIntStr: string): Uint8Array {
36
+ let hex = BigInt(bigIntStr.trim()).toString(16);
37
+ if (hex.length % 2 !== 0) {
38
+ hex = '0' + hex;
39
+ }
40
+ const len = hex.length / 2;
41
+ const u8 = new Uint8Array(len);
42
+ for (let i = 0; i < len; i++) {
43
+ u8[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
44
+ }
45
+ return u8;
46
+ }
47
+
48
+ function jp2ToDataURL(jp2Bytes: Uint8Array): string {
49
+ const jpx = new JpxImage();
50
+ const buf = Buffer.from(jp2Bytes);
51
+ jpx.parse(buf);
52
+
53
+ const width = jpx.width;
54
+ const height = jpx.height;
55
+ const componentsCount = jpx.componentsCount;
56
+ const tiles = jpx.tiles;
57
+ if (!tiles || tiles.length === 0) throw new Error("No tiles found in JP2");
58
+
59
+ const items = tiles[0].items;
60
+
61
+ const canvas = document.createElement('canvas');
62
+ canvas.width = width;
63
+ canvas.height = height;
64
+ const ctx = canvas.getContext('2d');
65
+ if (!ctx) throw new Error("Canvas context not available");
66
+
67
+ const imgData = ctx.createImageData(width, height);
68
+ const data = imgData.data;
69
+
70
+ if (componentsCount === 3 || componentsCount === 4) {
71
+ const step = componentsCount;
72
+ for (let i = 0, j = 0; i < items.length; i += step, j += 4) {
73
+ data[j] = items[i];
74
+ data[j + 1] = items[i + 1];
75
+ data[j + 2] = items[i + 2];
76
+ data[j + 3] = componentsCount === 4 ? items[i + 3] : 255;
77
+ }
78
+ } else if (componentsCount === 1) {
79
+ for (let i = 0, j = 0; i < items.length; i++, j += 4) {
80
+ const val = items[i];
81
+ data[j] = val;
82
+ data[j + 1] = val;
83
+ data[j + 2] = val;
84
+ data[j + 3] = 255;
85
+ }
86
+ } else {
87
+ throw new Error("Unsupported components count: " + componentsCount);
88
+ }
89
+
90
+ ctx.putImageData(imgData, 0, 0);
91
+ return canvas.toDataURL('image/png');
92
+ }
93
+
94
+ const UIDAI_PUBLIC_KEYS = [
95
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh1+zYnvbcEm0Yz73s5u42odpUJMr9wv5bVw7sOE5nFNbrB+U++5I0f8cL2HoHnJOkwvLZzrD0jG/vxAKi6vii/gjEzUEgrkdIHxMP3D6GJs0MSQHiEXvIGOwPIH3BLtBOc3m28NVNT6Q9iq0gUwuxnlhV38UdNhCllqNYhWmAMPJkImgaKrRZvY2pWNs6gd+PlAF/9SO69x3+1meA8kPk2ZvQanZlx9tfaExeOe9or3NQiKy2+UbtXrpcoAfYbbWi1OUzXi5bJdhbGp239c1fX6UKyUM5IUMY+m3I7wu2WQ7lmeO2n/vwzQz/PKHXPWYu3bydWMLdCi07vOQBqzCKwIDAQAB",
96
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv1DSK9/qrW8RX1vDZMsE8xiiyJlj+6xxDtu+nSZDW9C/iajSqJ2QgLvRgweTw4suzzxZQseOE+kbqlbesNHc0lQjt9T+CGYrUTCbMI/a3zZbr3vPxz3VlN7iqr8U6ISUN53x+6qAc4Z/Pc66IqJA6zXBPKFZiHHMmi00eM14HgNWrLEkYHE5geBmBgEevznskS4Q+sJVX+4seJ/zadc35O4G6gvWZatlsB5STGSdes4TqF1k0FV4a0CF7vAzpUA4EtQohl6dnKWpfWYAJUxbSrH1OCLFBn1ABe9Yw5iZkIMFYauhyFzP16XCiG91TPoORIJ8ssIR9uf21o6rD82OJwIDAQAB",
97
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmMIJKj28JcTN1B72p2/pgzDCoguhs/rbIXgN/ybNNh0NVOrZV2KllrmT5VOYlMrABpvIp7JU/n6hma3/O14n7nvngJ/y3colh8rk7msDwVAO7ZuVD+GCzfaYPLLkUS+wqH7M7FOHIn/pyJo1Rkxm98lO3dyox5RuLG2Uqm7JfVIomm0t7QKJoM5rf8JNvPXdwsxN89eWlT2Bf7BF//G3FKiF7ZHfvIyyqte/3orRRG/M80QqLrDP1RIeOa53ZTgILXcyQOb2yZOqNH3iN2uSKRsusNO17To5FOb2J9Hd5wIMuDv3zw4MWTrKAWuTYon90QSeGRKv1d5AQNRt0x5dSwIDAQAB",
98
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv0HjbFpvu/kR+gTI2+svGNmW4eZHhTVBG/N+byaq3GH0SDM+jO5RW4BbXNzaSKc0I5mIyN1vQf2KmNV/3Xai6MokiiZrBRfM8a497zCMteHTAzSP1L0DmohUuBQh/s1hfqRIIWpfEu7noW2G8toK0ZOQR1E0FtinWNtqEeuxlNEKgfxkN4/vRzgvGFw+PPcoG5uMdcd7/DjDE1i20zmT+55DgIBrneCwrW7nIM0Md3BPOTV8iBwzjdVcdDHhMtSpi9UKUHw80sDRZp7ygB4Z0QmhSxCMCg9g7KPHYY+PVRC2sFreZBC6rtmIL+HMUPciRCCqMZLx3f6xRSD97lZr/wIDAQAB",
99
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAonIsDl8t5bpwftk/A27CsfC5VZMjkPrMDwvL8gyAoVwIi0iGhmty6yWrC/VaL+Brae29XMg7dMdwnbIUHmwHxovN+FnT2vfz/O0kHQcgVdwVSIR0tFwsmC+pVKpSqm//skgYYcZQhdhLZBWOn0PZ81ymm0jOkwBSIQKkyuCTv/1HSwjTLR0EBvaH9+Vb0iaiOEv1ikHDhMOXTxx8URWBnJJt463z7LuZBMSG8fXVMDl3vqY1hDZzKbXBaK/clRIXMff0jUOvfPMfabHju+eUnceosQwL3eurq96+oHahz4FmrfBqikHe3xQ7/4NdvSvVuwth0kcsI0ptRBG8m1NglQIDAQAB"
100
+ ];
101
+
102
+ async function verifySignature(signedData: Uint8Array, signature: Uint8Array): Promise<boolean> {
103
+ try {
104
+ for (const pem of UIDAI_PUBLIC_KEYS) {
105
+ const binaryDerString = window.atob(pem);
106
+ const binaryDer = new Uint8Array(binaryDerString.length);
107
+ for (let i = 0; i < binaryDerString.length; i++) {
108
+ binaryDer[i] = binaryDerString.charCodeAt(i);
109
+ }
110
+
111
+ try {
112
+ const key = await window.crypto.subtle.importKey(
113
+ "spki",
114
+ binaryDer.buffer as ArrayBuffer,
115
+ {
116
+ name: "RSASSA-PKCS1-v1_5",
117
+ hash: "SHA-256"
118
+ },
119
+ false,
120
+ ["verify"]
121
+ );
122
+
123
+ const isValid = await window.crypto.subtle.verify(
124
+ "RSASSA-PKCS1-v1_5",
125
+ key,
126
+ signature as any,
127
+ signedData as any
128
+ );
129
+
130
+ if (isValid) return true;
131
+ } catch (err) {
132
+ // Continue to next key
133
+ }
134
+ }
135
+
136
+ return false;
137
+ } catch (e) {
138
+ console.error("Signature verification failed:", e);
139
+ return false;
140
+ }
141
+ }
142
+
143
+ export async function decodeAadhaarQR(qrString: string): Promise<DecodeResult> {
144
+ try {
145
+ const rawQr = qrString.trim();
146
+
147
+ // Handle old XML format
148
+ if (rawQr.startsWith('<')) {
149
+ const parser = new DOMParser();
150
+ const xmlDoc = parser.parseFromString(rawQr, "text/xml");
151
+
152
+ const parseError = xmlDoc.getElementsByTagName("parsererror");
153
+ if (parseError.length > 0) {
154
+ return { success: false, error: 'Failed to parse XML data' };
155
+ }
156
+
157
+ // The root element is usually PrintLetterBarcodeData
158
+ const root = xmlDoc.documentElement;
159
+
160
+ const result: AadhaarData = {};
161
+
162
+ const fieldMap: Record<string, keyof AadhaarData> = {
163
+ "uid": "aadhaar_last4", // We'll extract last 4 below
164
+ "name": "name",
165
+ "gender": "gender",
166
+ "yob": "dob", // Fallback if dob isn't present
167
+ "dob": "dob",
168
+ "co": "care_of",
169
+ "house": "house",
170
+ "street": "street",
171
+ "lm": "landmark",
172
+ "loc": "location",
173
+ "vtc": "vtc",
174
+ "po": "post_office",
175
+ "dist": "district",
176
+ "subdist": "sub_district",
177
+ "state": "state",
178
+ "pc": "pincode"
179
+ };
180
+
181
+ for (const [xmlKey, resKey] of Object.entries(fieldMap)) {
182
+ const val = root.getAttribute(xmlKey);
183
+ if (val) {
184
+ (result as any)[resKey] = val;
185
+ }
186
+ }
187
+
188
+ // Format Aadhaar Last 4 if full UID was provided
189
+ if (root.getAttribute("uid") && root.getAttribute("uid")!.length >= 4) {
190
+ result.aadhaar_last4 = root.getAttribute("uid")!.slice(-4);
191
+ }
192
+
193
+ // Old XML doesn't have signature or photo
194
+ return { success: true, data: result };
195
+ }
196
+
197
+ // Handle Secure QR Format (Big Integer)
198
+ const isSecure = rawQr.match(/^\d{100,}$/);
199
+ if (!isSecure) {
200
+ return { success: false, error: 'Invalid QR string format.' };
201
+ }
202
+
203
+ const rawBytes = bigIntToBytes(rawQr);
204
+
205
+ let decompressed: Uint8Array | null = null;
206
+ const windowBitsOptions = [15, -15, 31, 47];
207
+
208
+ // Try decompressing with different padded lengths
209
+ for (let pad = 0; pad < 4; pad++) {
210
+ let paddedBytes = rawBytes;
211
+ if (pad > 0) {
212
+ paddedBytes = new Uint8Array(rawBytes.length + pad);
213
+ paddedBytes.set(rawBytes, pad);
214
+ }
215
+
216
+ for (const wbits of windowBitsOptions) {
217
+ try {
218
+ decompressed = pako.inflate(paddedBytes, { windowBits: wbits });
219
+ break;
220
+ } catch (e) {
221
+ // Ignore and try next
222
+ }
223
+ }
224
+ if (decompressed) break;
225
+ }
226
+
227
+ if (!decompressed) {
228
+ return { success: false, error: 'Failed to decompress Aadhaar data (Zlib error)' };
229
+ }
230
+
231
+ const signedData = decompressed.slice(0, -256);
232
+ const signature = decompressed.slice(-256);
233
+ const data = signedData;
234
+
235
+ const DELIMITER = 255;
236
+ let parts: string[] = [];
237
+ let start = 0;
238
+ let dob_idx = -1;
239
+
240
+ for (let i = 0; i < data.length; i++) {
241
+ if (data[i] === DELIMITER) {
242
+ const valBytes = data.slice(start, i);
243
+ // Convert Latin-1 (ISO-8859-1) bytes to string
244
+ const val = Array.from(valBytes).map(b => String.fromCharCode(b)).join('');
245
+ parts.push(val);
246
+ start = i + 1;
247
+
248
+ if (dob_idx === -1 && ['M', 'F', 'T'].includes(val) && parts.length > 3) {
249
+ dob_idx = parts.length - 2;
250
+ }
251
+
252
+ if (dob_idx !== -1 && parts.length === dob_idx + 13) {
253
+ start = i + 1;
254
+ break;
255
+ }
256
+ }
257
+ }
258
+
259
+ const result: AadhaarData = {};
260
+ result.email_mobile_indicator = parts[0];
261
+
262
+ if (dob_idx !== -1) {
263
+ const getP = (i: number) => i < parts.length ? parts[i].trim() : "";
264
+ result.reference_id = getP(dob_idx - 2);
265
+ result.name = getP(dob_idx - 1);
266
+ result.dob = getP(dob_idx);
267
+ result.gender = getP(dob_idx + 1);
268
+ result.care_of = getP(dob_idx + 2);
269
+ result.district = getP(dob_idx + 3);
270
+ result.landmark = getP(dob_idx + 4);
271
+ result.house = getP(dob_idx + 5);
272
+ result.location = getP(dob_idx + 6);
273
+ result.pincode = getP(dob_idx + 7);
274
+ result.post_office = getP(dob_idx + 8);
275
+ result.state = getP(dob_idx + 9);
276
+ result.street = getP(dob_idx + 10);
277
+ result.sub_district = getP(dob_idx + 11);
278
+ result.vtc = getP(dob_idx + 12);
279
+ }
280
+
281
+ if (result.reference_id && result.reference_id.length >= 4) {
282
+ result.aadhaar_last4 = result.reference_id.substring(0, 4);
283
+ }
284
+
285
+ if (start < data.length) {
286
+ let tail = data.length;
287
+ const ind = parseInt(parts[0], 10) || 0;
288
+ if (ind === 3) tail -= 64;
289
+ else if (ind === 1 || ind === 2) tail -= 32;
290
+
291
+ let photoBytes = data.slice(start, tail);
292
+
293
+ let photoStart = -1;
294
+ for (let j = 0; j < photoBytes.length - 3; j++) {
295
+ if (photoBytes[j] === 0xFF && photoBytes[j+1] === 0x4F && photoBytes[j+2] === 0xFF && photoBytes[j+3] === 0x51) {
296
+ photoStart = j;
297
+ break;
298
+ }
299
+ }
300
+ if (photoStart === -1) {
301
+ for (let j = 0; j < photoBytes.length - 7; j++) {
302
+ if (photoBytes[j] === 0x00 && photoBytes[j+1] === 0x00 && photoBytes[j+2] === 0x00 && photoBytes[j+3] === 0x0C &&
303
+ photoBytes[j+4] === 0x6A && photoBytes[j+5] === 0x50 && photoBytes[j+6] === 0x20 && photoBytes[j+7] === 0x20) {
304
+ photoStart = j;
305
+ break;
306
+ }
307
+ }
308
+ }
309
+ if (photoStart !== -1) {
310
+ photoBytes = photoBytes.slice(photoStart);
311
+ }
312
+
313
+ try {
314
+ const dataUrl = jp2ToDataURL(photoBytes);
315
+ result.photo_base64 = dataUrl.split(',')[1];
316
+ result.photo_mime = "image/png";
317
+ } catch (err) {
318
+ console.error("Canvas JP2 decoding failed:", err);
319
+ const CHUNK_SIZE = 0x8000;
320
+ let c = [];
321
+ for (let i = 0; i < photoBytes.length; i += CHUNK_SIZE) {
322
+ c.push(String.fromCharCode.apply(null, Array.from(photoBytes.subarray(i, i + CHUNK_SIZE))));
323
+ }
324
+ result.photo_base64 = btoa(c.join(''));
325
+ result.photo_mime = "image/jp2";
326
+ }
327
+ }
328
+
329
+ result.signature_valid = await verifySignature(signedData, signature);
330
+
331
+ return { success: true, data: result };
332
+
333
+ } catch (error: any) {
334
+ return { success: false, error: error.message || 'Unknown error occurred during parsing' };
335
+ }
336
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './decoder';
2
+ // Export the React Component if they want to use it
3
+ // export { VirtualAadhaarCard } from './VirtualAadhaarCard';
package/tsup.config.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ clean: true,
8
+ external: ['react', 'react-dom', 'lucide-react'],
9
+ loader: {
10
+ '.png': 'dataurl',
11
+ },
12
+ });