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 +54 -0
- package/dist/index.d.mts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +294 -0
- package/dist/index.mjs +258 -0
- package/package.json +38 -0
- package/src/VirtualAadhaarCard.tsx +133 -0
- package/src/assets/card-bg.png +0 -0
- package/src/assets/emblem.png +0 -0
- package/src/decoder.ts +336 -0
- package/src/index.ts +3 -0
- package/tsup.config.ts +12 -0
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
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
package/tsup.config.ts
ADDED