Move self app store to mobile sdk (#1040)

This commit is contained in:
Aaron DeRuvo
2025-09-11 17:30:01 +02:00
committed by GitHub
parent f416211037
commit 1f362b33ce
35 changed files with 968 additions and 680 deletions

View File

@@ -55,10 +55,14 @@ export {
SelfAppBuilder,
bigIntToString,
brutforceSignatureAlgorithmDsc,
buildSMT,
calculateUserIdentifierHash,
findStartPubKeyIndex,
formatEndpoint,
formatMrz,
genAndInitMockPassportData,
genMockIdDoc,
genMockIdDocAndInitDataParsing,
generateCircuitInputsDSC,
generateCircuitInputsRegister,
generateCircuitInputsVCandDisclose,
@@ -69,28 +73,17 @@ export {
getLeafCscaTree,
getLeafDscTree,
getSKIPEM,
getSolidityPackedUserContextData,
getUniversalLink,
hashEndpointWithScope,
initElliptic,
initPassportDataParsing,
parseCertificateSimple,
parseDscCertificateData,
genMockIdDoc,
genMockIdDocAndInitDataParsing,
buildSMT,
calculateUserIdentifierHash,
getSolidityPackedUserContextData,
stringToBigInt,
} from './src/utils/index.js';
export {
prepareAadhaarRegisterTestData,
prepareAadhaarDiscloseTestData,
prepareAadhaarRegisterData,
} from './src/utils/aadhaar/mockData.js';
export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js';
export { createSelector } from './src/utils/aadhaar/constants.js';
// Hash utilities
export {
customHasher,
@@ -99,3 +92,11 @@ export {
hash,
packBytesAndPoseidon,
} from './src/utils/hash.js';
export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js';
export {
prepareAadhaarDiscloseTestData,
prepareAadhaarRegisterData,
prepareAadhaarRegisterTestData,
} from './src/utils/aadhaar/mockData.js';

View File

@@ -647,8 +647,8 @@
"build:watch": "tsup --watch",
"format": "prettier --write .",
"lint": "prettier --check .",
"lint:imports": "eslint . --fix",
"lint:imports:check": "eslint .",
"lint:imports": "yarn eslint --fix .",
"lint:imports:check": "yarn eslint .",
"nice": "yarn format && yarn lint:imports",
"nice:check": "yarn lint && yarn lint:imports:check",
"prepublishOnly": "yarn build",

View File

@@ -1,7 +1,9 @@
export type Country3LetterCode = keyof typeof countryCodes;
export type document_type = 'passport' | 'id_card';
export type hashAlgosTypes = 'sha512' | 'sha384' | 'sha256' | 'sha224' | 'sha1';
export const AADHAAR_ATTESTATION_ID = '3';
export const API_URL = 'https://api.self.xyz';
export const API_URL_STAGING = 'https://api.staging.self.xyz';
export const CHAIN_NAME = 'celo';
@@ -42,8 +44,6 @@ export const CSCA_TREE_URL_STAGING = 'https://tree.staging.self.xyz/csca';
export const CSCA_TREE_URL_STAGING_ID_CARD = 'https://tree.staging.self.xyz/csca-id';
export const AADHAAR_ATTESTATION_ID = '3';
// we make it global here because passing it to generateCircuitInputsRegister caused trouble
export const DEFAULT_MAJORITY = '18';

View File

@@ -1,4 +1,5 @@
import * as fs from 'fs';
import { buildAadhaarSMT } from '../trees.js';
async function build_aadhaar_ofac_smt() {

View File

@@ -1,3 +1,4 @@
/* eslint-disable sort-exports/sort-exports */
export const MAX_FIELD_BYTE_SIZE = 31;
export const NAME_MAX_LENGTH = 2 * MAX_FIELD_BYTE_SIZE; // 62 bytes
export const TOTAL_REVEAL_DATA_LENGTH = 119;

View File

@@ -1,29 +1,34 @@
import { calculateAge, generateTestData, testCustomData } from './utils.js';
import {
convertBigIntToByteArray,
decompressByteArray,
splitToWords,
extractPhoto,
} from '@anon-aadhaar/core';
import { bufferToHex, Uint8ArrayToCharArray } from '@zk-email/helpers/dist/binary-format.js';
import { sha256Pad } from '@zk-email/helpers/dist/sha-utils.js';
import { testQRData } from './assets/dataInput.js';
import { stringToAsciiArray } from './utils.js';
import { packBytesAndPoseidon } from '../hash.js';
import { poseidon5 } from 'poseidon-lite';
import forge from 'node-forge';
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
import { SMT } from '@openpassport/zk-kit-smt';
import { poseidon5 } from 'poseidon-lite';
import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js';
import { findIndexInTree, formatInput } from '../circuits/generateInputs.js';
import { packBytesAndPoseidon } from '../hash.js';
import {
generateMerkleProof,
generateSMTProof,
getNameDobLeafAadhaar,
getNameYobLeafAahaar,
} from '../trees.js';
import { testQRData } from './assets/dataInput.js';
import {
calculateAge,
extractQRDataFields,
generateTestData,
stringToAsciiArray,
testCustomData,
} from './utils.js';
import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js';
import { extractQRDataFields } from './utils.js';
import {
convertBigIntToByteArray,
decompressByteArray,
extractPhoto,
splitToWords,
} from '@anon-aadhaar/core';
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
import { SMT } from '@openpassport/zk-kit-smt';
import { bufferToHex, Uint8ArrayToCharArray } from '@zk-email/helpers/dist/binary-format.js';
import { sha256Pad } from '@zk-email/helpers/dist/sha-utils.js';
// Helper function to compute padded name
function computePaddedName(name: string): number[] {
@@ -163,166 +168,6 @@ function processQRDataSimple(qrData: string) {
};
}
export function prepareAadhaarRegisterTestData(
privKeyPem: string,
pubkeyPem: string,
secret: string,
name?: string,
dateOfBirth?: string,
gender?: string,
pincode?: string,
state?: string,
timestamp?: string
) {
const sharedData = processQRData(
privKeyPem,
name,
dateOfBirth,
gender,
pincode,
state,
timestamp
);
const delimiterIndices: number[] = [];
for (let i = 0; i < sharedData.qrDataPadded.length; i++) {
if (sharedData.qrDataPadded[i] === 255) {
delimiterIndices.push(i);
}
if (delimiterIndices.length === 18) {
break;
}
}
let photoEOI = 0;
for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) {
if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) {
photoEOI = i + 1;
}
}
if (photoEOI === 0) {
throw new Error('Photo EOI not found');
}
const signatureBytes = sharedData.decodedData.slice(
sharedData.decodedData.length - 256,
sharedData.decodedData.length
);
const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString());
const publicKey = forge.pki.publicKeyFromPem(pubkeyPem);
const modulusHex = publicKey.n.toString(16);
const pubKey = BigInt('0x' + modulusHex);
const nullifier = nullifierHash(sharedData.extractedFields);
const packedCommitment = computePackedCommitment(sharedData.extractedFields);
const commitment = computeCommitment(
BigInt(secret),
BigInt(sharedData.qrHash),
nullifier,
packedCommitment,
BigInt(sharedData.photoHash)
);
const inputs = {
qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded),
qrDataPaddedLength: sharedData.qrDataPaddedLen,
delimiterIndices: delimiterIndices,
signature: splitToWords(signature, BigInt(121), BigInt(17)),
pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)),
secret: secret,
photoEOI: photoEOI,
};
return {
inputs,
nullifier,
commitment,
};
}
export async function prepareAadhaarRegisterData(qrData: string, secret: string, certs: string[]) {
const sharedData = processQRDataSimple(qrData);
const delimiterIndices: number[] = [];
for (let i = 0; i < sharedData.qrDataPadded.length; i++) {
if (sharedData.qrDataPadded[i] === 255) {
delimiterIndices.push(i);
}
if (delimiterIndices.length === 18) {
break;
}
}
let photoEOI = 0;
for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) {
if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) {
photoEOI = i + 1;
}
}
if (photoEOI === 0) {
throw new Error('Photo EOI not found');
}
const signatureBytes = sharedData.decodedData.slice(
sharedData.decodedData.length - 256,
sharedData.decodedData.length
);
const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString());
//do promise.all for all certs and pick the one that is valid
const certificates = await Promise.all(
certs.map(async (cert) => {
const certificate = forge.pki.certificateFromPem(cert);
const publicKey = certificate.publicKey as forge.pki.rsa.PublicKey;
try {
const md = forge.md.sha256.create();
md.update(forge.util.binary.raw.encode(sharedData.signedData));
const isValid = publicKey.verify(md.digest().getBytes(), signatureBytes);
return isValid;
} catch (error) {
return false;
}
})
);
//find the valid cert
const validCert = certificates.indexOf(true);
if (validCert === -1) {
throw new Error('No valid certificate found');
}
const certPem = certs[validCert];
const cert = forge.pki.certificateFromPem(certPem);
const modulusHex = (cert.publicKey as forge.pki.rsa.PublicKey).n.toString(16);
const pubKey = BigInt('0x' + modulusHex);
const nullifier = nullifierHash(sharedData.extractedFields);
const packedCommitment = computePackedCommitment(sharedData.extractedFields);
const commitment = computeCommitment(
BigInt(secret),
BigInt(sharedData.qrHash),
nullifier,
packedCommitment,
BigInt(sharedData.photoHash)
);
const inputs = {
qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded),
qrDataPaddedLength: sharedData.qrDataPaddedLen,
delimiterIndices: delimiterIndices,
signature: splitToWords(signature, BigInt(121), BigInt(17)),
pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)),
secret: secret,
photoEOI: photoEOI,
};
return {
inputs,
nullifier,
commitment,
};
}
export function prepareAadhaarDiscloseTestData(
privateKeyPem: string,
merkletree: LeanIMT,
@@ -444,3 +289,163 @@ export function prepareAadhaarDiscloseTestData(
commitment,
};
}
export async function prepareAadhaarRegisterData(qrData: string, secret: string, certs: string[]) {
const sharedData = processQRDataSimple(qrData);
const delimiterIndices: number[] = [];
for (let i = 0; i < sharedData.qrDataPadded.length; i++) {
if (sharedData.qrDataPadded[i] === 255) {
delimiterIndices.push(i);
}
if (delimiterIndices.length === 18) {
break;
}
}
let photoEOI = 0;
for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) {
if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) {
photoEOI = i + 1;
}
}
if (photoEOI === 0) {
throw new Error('Photo EOI not found');
}
const signatureBytes = sharedData.decodedData.slice(
sharedData.decodedData.length - 256,
sharedData.decodedData.length
);
const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString());
//do promise.all for all certs and pick the one that is valid
const certificates = await Promise.all(
certs.map(async (cert) => {
const certificate = forge.pki.certificateFromPem(cert);
const publicKey = certificate.publicKey as forge.pki.rsa.PublicKey;
try {
const md = forge.md.sha256.create();
md.update(forge.util.binary.raw.encode(sharedData.signedData));
const isValid = publicKey.verify(md.digest().getBytes(), signatureBytes);
return isValid;
} catch (error) {
return false;
}
})
);
//find the valid cert
const validCert = certificates.indexOf(true);
if (validCert === -1) {
throw new Error('No valid certificate found');
}
const certPem = certs[validCert];
const cert = forge.pki.certificateFromPem(certPem);
const modulusHex = (cert.publicKey as forge.pki.rsa.PublicKey).n.toString(16);
const pubKey = BigInt('0x' + modulusHex);
const nullifier = nullifierHash(sharedData.extractedFields);
const packedCommitment = computePackedCommitment(sharedData.extractedFields);
const commitment = computeCommitment(
BigInt(secret),
BigInt(sharedData.qrHash),
nullifier,
packedCommitment,
BigInt(sharedData.photoHash)
);
const inputs = {
qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded),
qrDataPaddedLength: sharedData.qrDataPaddedLen,
delimiterIndices: delimiterIndices,
signature: splitToWords(signature, BigInt(121), BigInt(17)),
pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)),
secret: secret,
photoEOI: photoEOI,
};
return {
inputs,
nullifier,
commitment,
};
}
export function prepareAadhaarRegisterTestData(
privKeyPem: string,
pubkeyPem: string,
secret: string,
name?: string,
dateOfBirth?: string,
gender?: string,
pincode?: string,
state?: string,
timestamp?: string
) {
const sharedData = processQRData(
privKeyPem,
name,
dateOfBirth,
gender,
pincode,
state,
timestamp
);
const delimiterIndices: number[] = [];
for (let i = 0; i < sharedData.qrDataPadded.length; i++) {
if (sharedData.qrDataPadded[i] === 255) {
delimiterIndices.push(i);
}
if (delimiterIndices.length === 18) {
break;
}
}
let photoEOI = 0;
for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) {
if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) {
photoEOI = i + 1;
}
}
if (photoEOI === 0) {
throw new Error('Photo EOI not found');
}
const signatureBytes = sharedData.decodedData.slice(
sharedData.decodedData.length - 256,
sharedData.decodedData.length
);
const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString());
const publicKey = forge.pki.publicKeyFromPem(pubkeyPem);
const modulusHex = publicKey.n.toString(16);
const pubKey = BigInt('0x' + modulusHex);
const nullifier = nullifierHash(sharedData.extractedFields);
const packedCommitment = computePackedCommitment(sharedData.extractedFields);
const commitment = computeCommitment(
BigInt(secret),
BigInt(sharedData.qrHash),
nullifier,
packedCommitment,
BigInt(sharedData.photoHash)
);
const inputs = {
qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded),
qrDataPaddedLength: sharedData.qrDataPaddedLen,
delimiterIndices: delimiterIndices,
signature: splitToWords(signature, BigInt(121), BigInt(17)),
pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)),
secret: secret,
photoEOI: photoEOI,
};
return {
inputs,
nullifier,
commitment,
};
}

View File

@@ -1,27 +1,40 @@
import forge from 'node-forge';
import {
convertBigIntToByteArray,
decompressByteArray,
returnFullId,
extractPhoto,
getEndIndex,
getRandomBytes,
IdFields,
rawDataToCompressedQR,
replaceBytesBetween,
IdFields,
extractPhoto,
getRandomBytes,
getEndIndex,
returnFullId,
} from '@anon-aadhaar/core';
import forge from 'node-forge';
export function stringToAsciiArray(str: string) {
return str.split('').map((char) => char.charCodeAt(0));
export interface ExtractedQRData {
name: string;
yob: string;
mob: string;
dob: string;
gender: string;
pincode: string;
state: string;
aadhaarLast4Digits: string;
phoneNoLast4Digits: string;
timestamp: string;
}
// This is the official test data issued by the UIDAI
// In this script we'll change the signed data to emulate the specs of the Aadhaar QR V2
// and sign the data again with our own certificates.
// data on https://uidai.gov.in/en/ecosystem/authentication-devices-documents/qr-code-reader.html
// This data is copied from https://github.dev/anon-aadhaar/anon-aadhaar/blob/main/packages/circuits/src/helpers/extractor.circom
export const testCustomData =
'2374971804270526477833002468783965837992554564899874087591661303561346432389832047870524302186901344489362368642972767716416349990805756094923115719687656090691368051627957878187788907419297818953295185555346288172578594637886352753543271000481717080003254556962148594350559820352806251787713278744047402230989238559317351232114240089849934148895256488140236015024800731753594740948640957680138566468247224859669467819596919398964809164399637893729212452791889199675715949918925838319591794702333094022248132120531152523331442741730158840977243402215102904932650832502847295644794421419704633765033761284508863534321317394686768650111457751139630853448637215423705157211510636160227953566227527799608082928846103264491539001327407775670834868948113753614112563650255058316849200536533335903554984254814901522086937767458409075617572843449110393213525925388131214952874629655799772119820372255291052673056372346072235458198199995637720424196884145247220163810790179386390283738429482893152518286247124911446073389185062482901364671389605727763080854673156754021728522287806275420847159574631844674460263574901590412679291518508010087116598357407343835408554094619585212373168435612645646129147973594416508676872819776522537778717985070402222824965034768103900739105784663244748432502180989441389718131079445941981681118258324511923246198334046020123727749408128519721102477302359413240175102907322619462289965085963377744024233678337951462006962521823224880199210318367946130004264196899778609815012001799773327514133268825910089483612283510244566484854597156100473055413090101948456959122378865704840756793122956663218517626099291311352417342899623681483097817511136427210593032393600010728324905512596767095096153856032112835755780472808814199620390836980020899858288860556611564167406292139646289142056168261133256777093245980048335918156712295254776487472431445495668303900536289283098315798552328294391152828182614909451410115516297083658174657554955228963550255866282688308751041517464999930825273776417639569977754844191402927594739069037851707477839207593911886893016618794870530622356073909077832279869798641545167528509966656120623184120128052588408742941658045827255866966100249857968956536613250770326334844204927432961924987891433020671754710428050564671868464658436926086493709176888821257183419013229795869757265111599482263223604228286513011751601176504567030118257385997460972803240338899836840030438830725520798480181575861397469056536579877274090338750406459700907704031830137890544492015701251066934352867527112361743047684237105216779177819594030160887368311805926405114938744235859610328064947158936962470654636736991567663705830950312548447653861922078087824048793236971354828540758657075837209006713701763902429652486225300535997260665898927924843608750347193892239342462507130025307878412116604096773706728162016134101751551184021079984480254041743057914746472840768175369369852937574401874295943063507273467384747124843744395375119899278823903202010381949145094804675442110869084589592876721655764753871572233276245590041302887094585204427900634246823674277680009401177473636685542700515621164233992970974893989913447733956146698563285998205950467321954304';
export const FIELD_POSITIONS = {
REFERENCE_ID: 2,
NAME: 3,
DOB: 4,
GENDER: 5,
PINCODE: 11,
STATE: 13,
PHONE_NO: 17,
PHOTO: 18,
} as const;
// Will sign the data with the keys generated for test
const signNewTestData = (newSignedData: Uint8Array, privKeyPem: string) => {
@@ -45,65 +58,33 @@ const signNewTestData = (newSignedData: Uint8Array, privKeyPem: string) => {
}
};
export const generateTestData = ({
privKeyPem,
data,
dob,
gender,
pincode,
state,
photo,
name,
timestamp,
}: {
privKeyPem: string;
data: string;
dob?: string;
gender?: string;
pincode?: string;
state?: string;
photo?: boolean;
name?: string;
timestamp?: string;
}) => {
const qrDataBytes = convertBigIntToByteArray(BigInt(data));
const decodedData = decompressByteArray(qrDataBytes);
export function calculateAge(
dob: string,
mob: string,
yob: string
): { age: number; currentYear: number; currentMonth: number; currentDay: number } {
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth() + 1; // getMonth() returns 0-11
const currentDay = currentDate.getDate();
// Turning test data V1 into V2
// Adding the version specifier prefix,
// the last 4 digits of phone number and timestamp to now
const dataToSign = createCustomV2TestData({
signedData: decodedData.slice(0, decodedData.length - 256),
dob,
pincode,
gender,
state,
photo,
name,
timestamp,
});
const birthYear = parseInt(yob);
const birthMonth = parseInt(mob);
const birthDay = parseInt(dob);
// Signing the newly generated testData
const signature = signNewTestData(dataToSign, privKeyPem);
let age = currentYear - birthYear;
// Reconstructing the whole QR data
const tempData = Buffer.concat([dataToSign, signature]);
// Compressing the data to have it in the same format as the QR code
const newCompressedData = rawDataToCompressedQR(tempData);
const newQrData = {
testQRData: newCompressedData.toString(),
...returnFullId(dataToSign),
if (currentMonth < birthMonth || (currentMonth === birthMonth && currentDay < birthDay)) {
age--;
}
return {
age,
currentYear,
currentMonth,
currentDay,
};
}
return newQrData;
};
// This modify the test data to make it compliant with the secure Aadhaar QR V2 2022
// - Adds the version specifier at the beginning 'V2'
// - Mocks last 4 digits of phone number '1234' after VTC
// - Refresh timestamp data to now
// - Optionally it can take parameters to change the test data fields (dob, pinCode, gender, state)
export const createCustomV2TestData = ({
signedData,
dob,
@@ -238,99 +219,6 @@ export const createCustomV2TestData = ({
return newData;
};
export function calculateAge(
dob: string,
mob: string,
yob: string
): { age: number; currentYear: number; currentMonth: number; currentDay: number } {
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth() + 1; // getMonth() returns 0-11
const currentDay = currentDate.getDate();
const birthYear = parseInt(yob);
const birthMonth = parseInt(mob);
const birthDay = parseInt(dob);
let age = currentYear - birthYear;
if (currentMonth < birthMonth || (currentMonth === birthMonth && currentDay < birthDay)) {
age--;
}
return {
age,
currentYear,
currentMonth,
currentDay,
};
}
export function returnNewDateString(timestamp?: string): string {
const newDate = timestamp ? new Date(+timestamp) : new Date();
// Convert the UTC date to IST by adding 5 hours and 30 minutes
const offsetHours = 5;
const offsetMinutes = 30;
newDate.setUTCHours(newDate.getUTCHours() + offsetHours);
newDate.setUTCMinutes(newDate.getUTCMinutes() + offsetMinutes);
return (
newDate.getUTCFullYear().toString() +
(newDate.getUTCMonth() + 1).toString().padStart(2, '0') +
newDate.getUTCDate().toString().padStart(2, '0') +
newDate.getUTCHours().toString().padStart(2, '0') +
newDate.getUTCMinutes().toString().padStart(2, '0') +
newDate.getUTCSeconds().toString().padStart(2, '0') +
newDate.getUTCMilliseconds().toString().padStart(3, '0')
);
}
export const FIELD_POSITIONS = {
REFERENCE_ID: 2,
NAME: 3,
DOB: 4,
GENDER: 5,
PINCODE: 11,
STATE: 13,
PHONE_NO: 17,
PHOTO: 18,
} as const;
function asciiArrayToString(asciiArray: number[]): string {
return asciiArray
.filter((byte) => byte !== 0)
.map((byte) => String.fromCharCode(byte))
.join('');
}
function extractFieldData(
data: Uint8Array,
delimiterIndices: number[],
position: number
): number[] {
const startIndex = delimiterIndices[position - 1] + 1;
const endIndex = delimiterIndices[position];
const fieldData: number[] = [];
for (let i = startIndex; i < endIndex; i++) {
fieldData.push(data[i]);
}
return fieldData;
}
export interface ExtractedQRData {
name: string;
yob: string;
mob: string;
dob: string;
gender: string;
pincode: string;
state: string;
aadhaarLast4Digits: string;
phoneNoLast4Digits: string;
timestamp: string;
}
export function extractQRDataFields(qrData: string | Uint8Array): ExtractedQRData {
let qrDataBytes: Uint8Array;
@@ -428,3 +316,117 @@ export function extractQRDataFields(qrData: string | Uint8Array): ExtractedQRDat
timestamp,
};
}
export const generateTestData = ({
privKeyPem,
data,
dob,
gender,
pincode,
state,
photo,
name,
timestamp,
}: {
privKeyPem: string;
data: string;
dob?: string;
gender?: string;
pincode?: string;
state?: string;
photo?: boolean;
name?: string;
timestamp?: string;
}) => {
const qrDataBytes = convertBigIntToByteArray(BigInt(data));
const decodedData = decompressByteArray(qrDataBytes);
// Turning test data V1 into V2
// Adding the version specifier prefix,
// the last 4 digits of phone number and timestamp to now
const dataToSign = createCustomV2TestData({
signedData: decodedData.slice(0, decodedData.length - 256),
dob,
pincode,
gender,
state,
photo,
name,
timestamp,
});
// Signing the newly generated testData
const signature = signNewTestData(dataToSign, privKeyPem);
// Reconstructing the whole QR data
const tempData = Buffer.concat([dataToSign, signature]);
// Compressing the data to have it in the same format as the QR code
const newCompressedData = rawDataToCompressedQR(tempData);
const newQrData = {
testQRData: newCompressedData.toString(),
...returnFullId(dataToSign),
};
return newQrData;
};
export function returnNewDateString(timestamp?: string): string {
const newDate = timestamp ? new Date(+timestamp) : new Date();
// Convert the UTC date to IST by adding 5 hours and 30 minutes
const offsetHours = 5;
const offsetMinutes = 30;
newDate.setUTCHours(newDate.getUTCHours() + offsetHours);
newDate.setUTCMinutes(newDate.getUTCMinutes() + offsetMinutes);
return (
newDate.getUTCFullYear().toString() +
(newDate.getUTCMonth() + 1).toString().padStart(2, '0') +
newDate.getUTCDate().toString().padStart(2, '0') +
newDate.getUTCHours().toString().padStart(2, '0') +
newDate.getUTCMinutes().toString().padStart(2, '0') +
newDate.getUTCSeconds().toString().padStart(2, '0') +
newDate.getUTCMilliseconds().toString().padStart(3, '0')
);
}
function asciiArrayToString(asciiArray: number[]): string {
return asciiArray
.filter((byte) => byte !== 0)
.map((byte) => String.fromCharCode(byte))
.join('');
}
function extractFieldData(
data: Uint8Array,
delimiterIndices: number[],
position: number
): number[] {
const startIndex = delimiterIndices[position - 1] + 1;
const endIndex = delimiterIndices[position];
const fieldData: number[] = [];
for (let i = startIndex; i < endIndex; i++) {
fieldData.push(data[i]);
}
return fieldData;
}
// This is the official test data issued by the UIDAI
// In this script we'll change the signed data to emulate the specs of the Aadhaar QR V2
// and sign the data again with our own certificates.
// data on https://uidai.gov.in/en/ecosystem/authentication-devices-documents/qr-code-reader.html
// This data is copied from https://github.dev/anon-aadhaar/anon-aadhaar/blob/main/packages/circuits/src/helpers/extractor.circom
export function stringToAsciiArray(str: string) {
return str.split('').map((char) => char.charCodeAt(0));
}
// This modify the test data to make it compliant with the secure Aadhaar QR V2 2022
// - Adds the version specifier at the beginning 'V2'
// - Mocks last 4 digits of phone number '1234' after VTC
// - Refresh timestamp data to now
// - Optionally it can take parameters to change the test data fields (dob, pinCode, gender, state)
export const testCustomData =
'2374971804270526477833002468783965837992554564899874087591661303561346432389832047870524302186901344489362368642972767716416349990805756094923115719687656090691368051627957878187788907419297818953295185555346288172578594637886352753543271000481717080003254556962148594350559820352806251787713278744047402230989238559317351232114240089849934148895256488140236015024800731753594740948640957680138566468247224859669467819596919398964809164399637893729212452791889199675715949918925838319591794702333094022248132120531152523331442741730158840977243402215102904932650832502847295644794421419704633765033761284508863534321317394686768650111457751139630853448637215423705157211510636160227953566227527799608082928846103264491539001327407775670834868948113753614112563650255058316849200536533335903554984254814901522086937767458409075617572843449110393213525925388131214952874629655799772119820372255291052673056372346072235458198199995637720424196884145247220163810790179386390283738429482893152518286247124911446073389185062482901364671389605727763080854673156754021728522287806275420847159574631844674460263574901590412679291518508010087116598357407343835408554094619585212373168435612645646129147973594416508676872819776522537778717985070402222824965034768103900739105784663244748432502180989441389718131079445941981681118258324511923246198334046020123727749408128519721102477302359413240175102907322619462289965085963377744024233678337951462006962521823224880199210318367946130004264196899778609815012001799773327514133268825910089483612283510244566484854597156100473055413090101948456959122378865704840756793122956663218517626099291311352417342899623681483097817511136427210593032393600010728324905512596767095096153856032112835755780472808814199620390836980020899858288860556611564167406292139646289142056168261133256777093245980048335918156712295254776487472431445495668303900536289283098315798552328294391152828182614909451410115516297083658174657554955228963550255866282688308751041517464999930825273776417639569977754844191402927594739069037851707477839207593911886893016618794870530622356073909077832279869798641545167528509966656120623184120128052588408742941658045827255866966100249857968956536613250770326334844204927432961924987891433020671754710428050564671868464658436926086493709176888821257183419013229795869757265111599482263223604228286513011751601176504567030118257385997460972803240338899836840030438830725520798480181575861397469056536579877274090338750406459700907704031830137890544492015701251066934352867527112361743047684237105216779177819594030160887368311805926405114938744235859610328064947158936962470654636736991567663705830950312548447653861922078087824048793236971354828540758657075837209006713701763902429652486225300535997260665898927924843608750347193892239342462507130025307878412116604096773706728162016134101751551184021079984480254041743057914746472840768175369369852937574401874295943063507273467384747124843744395375119899278823903202010381949145094804675442110869084589592876721655764753871572233276245590041302887094585204427900634246823674277680009401177473636685542700515621164233992970974893989913447733956146698563285998205950467321954304';

View File

@@ -1,12 +1,12 @@
import { MAX_BYTES_IN_FIELD } from '../constants/constants.js';
export function bigIntToChunkedBytes(
num: BigInt | bigint,
num: bigint | bigint,
bytesPerChunk: number,
numChunks: number
) {
const res: string[] = [];
const bigintNum: bigint = typeof num == 'bigint' ? num : num.valueOf();
const bigintNum: bigint = typeof num == 'bigint' ? num : BigInt(num);
const msk = (1n << BigInt(bytesPerChunk)) - 1n;
for (let i = 0; i < numChunks; ++i) {
res.push(((bigintNum >> BigInt(i * bytesPerChunk)) & msk).toString());

View File

@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest';
import { calculateUserIdentifierHash } from './hash';
describe('calculateUserIdentifierHash', () => {
it('should return a bigint', () => {
const result = calculateUserIdentifierHash(
1,
'550e8400-e29b-41d4-a716-446655440000',
'some data'
);
expect(typeof result).toBe('bigint');
});
it('should return the same hash for identical inputs', () => {
const destChainID = 42;
const userID = 'abcdef12-3456-7890-abcd-ef1234567890';
const userDefinedData = 'Test data';
const hash1 = calculateUserIdentifierHash(destChainID, userID, userDefinedData);
const hash2 = calculateUserIdentifierHash(destChainID, userID, userDefinedData);
expect(hash1).toBe(hash2);
expect(hash1).toMatchInlineSnapshot(`525133570835708563534412370019423387022853755228n`);
});
it('should return different hash for different inputs', () => {
const hash1 = calculateUserIdentifierHash(
42,
'abcdef12-3456-7890-abcd-ef1234567890',
'Test data'
);
const hash2 = calculateUserIdentifierHash(
42,
'abcdef12-3456-7890-abcd-ef1234567890',
'Different data'
);
expect(hash1).not.toBe(hash2);
expect(hash1).toMatchInlineSnapshot(`525133570835708563534412370019423387022853755228n`);
});
it('should handle user ids starting with 0x', () => {
const hash1 = calculateUserIdentifierHash(42, '0xabcdef1234567890', 'Test data');
const hash2 = calculateUserIdentifierHash(42, 'abcdef1234567890', 'Test data');
expect(hash1).toBe(hash2);
expect(hash1).toMatchInlineSnapshot(`830654111289877969679298811043657652615780822337n`);
});
});

View File

@@ -29,7 +29,7 @@ export function calculateUserIdentifierHash(
destChainID: number,
userID: string,
userDefinedData: string
): BigInt {
): bigint {
const solidityPackedUserContextData = getSolidityPackedUserContextData(
destChainID,
userID,
@@ -133,7 +133,8 @@ export function getSolidityPackedUserContextData(
['bytes32', 'bytes32', 'bytes'],
[
ethers.zeroPadValue(ethers.toBeHex(destChainID), 32),
ethers.zeroPadValue('0x' + userIdHex, 32),
ethers.zeroPadValue(userIdHex.startsWith('0x') ? userIdHex : '0x' + userIdHex, 32),
ethers.toUtf8Bytes(userDefinedData),
]
);

View File

@@ -2,14 +2,15 @@ import countries from 'i18n-iso-countries';
// @ts-ignore
import en from 'i18n-iso-countries/langs/en.json' with { type: 'json' };
import {
poseidon12,
poseidon13,
poseidon2,
poseidon3,
poseidon5,
poseidon6,
poseidon10,
poseidon12,
poseidon13,
} from 'poseidon-lite';
import {
CSCA_TREE_DEPTH,
DSC_TREE_DEPTH,
@@ -17,6 +18,7 @@ import {
max_dsc_bytes,
OFAC_TREE_LEVELS,
} from '../constants/constants.js';
import { packBytes } from './bytes.js';
import type { CertificateData } from './certificate_parsing/dataStructure.js';
import { parseCertificateSimple } from './certificate_parsing/parseCertificateSimple.js';
import { stringToAsciiBigIntArray } from './circuits/uuid.js';
@@ -26,7 +28,6 @@ import {
DscCertificateMetaData,
parseDscCertificateData,
} from './passports/passport_parsing/parseDscCertificateData.js';
import { packBytes } from './bytes.js';
import { IMT } from '@openpassport/zk-kit-imt';
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
@@ -36,6 +37,52 @@ import { SMT } from '@openpassport/zk-kit-smt';
// SideEffect here
countries.registerLocale(en);
//---------------------------
// AADHAAR
//---------------------------
export function buildAadhaarSMT(field: any[], treetype: string): [number, number, SMT] {
let count = 0;
let startTime = performance.now();
const hash2 = (childNodes: ChildNodes) =>
childNodes.length === 2 ? poseidon2(childNodes) : poseidon3(childNodes);
const tree = new SMT(hash2, true);
for (let i = 0; i < field.length; i++) {
const entry = field[i];
if (i !== 0) {
console.log('Processing', treetype, 'number', i, 'out of', field.length);
}
let leaf = BigInt(0);
let reverse_leaf = BigInt(0);
if (treetype == 'name_and_dob') {
leaf = processNameAndDobAadhaar(entry, i);
reverse_leaf = processNameAndDobAadhaar(entry, i, true);
} else if (treetype == 'name_and_yob') {
leaf = processNameAndYobAadhaar(entry, i);
reverse_leaf = processNameAndYobAadhaar(entry, i, true);
}
if (leaf == BigInt(0) || tree.createProof(leaf).membership) {
console.log('This entry already exists in the tree, skipping...');
continue;
}
count += 1;
tree.add(leaf, BigInt(1));
if (reverse_leaf == BigInt(0) || tree.createProof(reverse_leaf).membership) {
console.log('This entry already exists in the tree, skipping...');
continue;
}
tree.add(reverse_leaf, BigInt(1));
count += 1;
}
return [count, performance.now() - startTime, tree];
}
// SMT trees for 3 levels of matching :
// 1. Passport Number and Nationality tree : level 3 (Absolute Match)
// 2. Name and date of birth combo tree : level 2 (High Probability Match)
@@ -266,12 +313,6 @@ export function getLeafCscaTree(csca_parsed: CertificateData): string {
return getLeaf(csca_parsed, 'csca');
}
export function getLeafDscTree(dsc_parsed: CertificateData, csca_parsed: CertificateData): string {
const dscLeaf = getLeaf(dsc_parsed, 'dsc');
const cscaLeaf = getLeaf(csca_parsed, 'csca');
return poseidon2([dscLeaf, cscaLeaf]).toString();
}
function processPassportNoAndNationality(
passno: string,
nationality: string,
@@ -480,6 +521,12 @@ function processCountry(country1: string, country2: string, i: number) {
return leaf;
}
export function getLeafDscTree(dsc_parsed: CertificateData, csca_parsed: CertificateData): string {
const dscLeaf = getLeaf(dsc_parsed, 'dsc');
const cscaLeaf = getLeaf(csca_parsed, 'csca');
return poseidon2([dscLeaf, cscaLeaf]).toString();
}
export function getLeafDscTreeFromDscCertificateMetadata(
dscParsed: CertificateData,
dscMetaData: DscCertificateMetaData
@@ -501,6 +548,18 @@ export function getNameDobLeaf(
return generateSmallKey(poseidon2([getDobLeaf(dobMrz), getNameLeaf(nameMrz)]));
}
export const getNameDobLeafAadhaar = (name: string, year: string, month: string, day: string) => {
const paddedName = name
.toUpperCase()
.padEnd(62, '\0')
.split('')
.map((char) => char.charCodeAt(0));
const namePacked = packBytes(paddedName);
return generateSmallKey(
poseidon5([namePacked[0], namePacked[1], BigInt(year), BigInt(month), BigInt(day)])
);
};
export function getNameLeaf(nameMrz: (bigint | number)[], i?: number): bigint {
const middleChunks: bigint[] = [];
const chunks: (number | bigint)[][] = [];
@@ -544,76 +603,6 @@ export function getNameYobLeaf(
return generateSmallKey(poseidon2([getYearLeaf(yobMrz), getNameLeaf(nameMrz)]));
}
export function getPassportNumberAndNationalityLeaf(
passport: (bigint | number)[],
nationality: (bigint | number)[],
i?: number
): bigint {
if (passport.length !== 9) {
console.log('parsed passport length is not 9:', i, passport);
return;
}
if (nationality.length !== 3) {
console.log('parsed nationality length is not 3:', i, nationality);
return;
}
try {
const fullHash = poseidon12(passport.concat(nationality));
return generateSmallKey(fullHash);
} catch (err) {
console.log('err : passport', err, i, passport);
}
}
//---------------------------
// AADHAAR
//---------------------------
export function buildAadhaarSMT(field: any[], treetype: string): [number, number, SMT] {
let count = 0;
let startTime = performance.now();
const hash2 = (childNodes: ChildNodes) =>
childNodes.length === 2 ? poseidon2(childNodes) : poseidon3(childNodes);
const tree = new SMT(hash2, true);
for (let i = 0; i < field.length; i++) {
const entry = field[i];
if (i !== 0) {
console.log('Processing', treetype, 'number', i, 'out of', field.length);
}
let leaf = BigInt(0);
let reverse_leaf = BigInt(0);
if (treetype == 'name_and_dob') {
leaf = processNameAndDobAadhaar(entry, i);
reverse_leaf = processNameAndDobAadhaar(entry, i, true);
} else if (treetype == 'name_and_yob') {
leaf = processNameAndYobAadhaar(entry, i);
reverse_leaf = processNameAndYobAadhaar(entry, i, true);
}
if (leaf == BigInt(0) || tree.createProof(leaf).membership) {
console.log('This entry already exists in the tree, skipping...');
continue;
}
count += 1;
tree.add(leaf, BigInt(1));
if (reverse_leaf == BigInt(0) || tree.createProof(reverse_leaf).membership) {
console.log('This entry already exists in the tree, skipping...');
continue;
}
tree.add(reverse_leaf, BigInt(1));
count += 1;
}
return [count, performance.now() - startTime, tree];
}
const processNameAndDobAadhaar = (entry: any, i: number, reverse: boolean = false): bigint => {
let firstName = entry.First_Name;
let lastName = entry.Last_Name;
@@ -684,18 +673,6 @@ const processDobAadhaar = (year: string, month: string, day: string): bigint[] =
return [year, month, day].map(BigInt);
};
export const getNameDobLeafAadhaar = (name: string, year: string, month: string, day: string) => {
const paddedName = name
.toUpperCase()
.padEnd(62, '\0')
.split('')
.map((char) => char.charCodeAt(0));
const namePacked = packBytes(paddedName);
return generateSmallKey(
poseidon5([namePacked[0], namePacked[1], BigInt(year), BigInt(month), BigInt(day)])
);
};
export const getNameYobLeafAahaar = (name: string, year: string) => {
const paddedName = name
.toUpperCase()
@@ -706,3 +683,24 @@ export const getNameYobLeafAahaar = (name: string, year: string) => {
return generateSmallKey(poseidon3([namePacked[0], namePacked[1], BigInt(year)]));
};
export function getPassportNumberAndNationalityLeaf(
passport: (bigint | number)[],
nationality: (bigint | number)[],
i?: number
): bigint {
if (passport.length !== 9) {
console.log('parsed passport length is not 9:', i, passport);
return;
}
if (nationality.length !== 3) {
console.log('parsed nationality length is not 3:', i, nationality);
return;
}
try {
const fullHash = poseidon12(passport.concat(nationality));
return generateSmallKey(fullHash);
} catch (err) {
console.log('err : passport', err, i, passport);
}
}