diff --git a/app/App.tsx b/app/App.tsx index 1ff5ea1a7..9acfab021 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -38,7 +38,7 @@ import { config } from "@gluestack-ui/config" // Optional if you want to use def // @ts-ignore import PassportReader from 'react-native-passport-reader'; -import {checkInputs, getFirstName} from './utils/checks'; +import {getFirstName, formatDuration, formatProof} from './utils/utils'; import { DEFAULT_PNUMBER, DEFAULT_DOB, @@ -46,9 +46,17 @@ import { DEFAULT_ADDRESS, LOCAL_IP, } from '@env'; -import {DataHash, PassportData} from './types/passportData'; -import {arraysAreEqual, bytesToBigDecimal, dataHashesObjToArray, formatAndConcatenateDataHashes, formatDuration, formatMrz, formatProof, splitToWords} from './utils/utils'; -import {hash, toUnsignedByte} from './utils/computeEContent'; +import {DataHash, PassportData} from '../common/src/utils/types'; +import { + hash, + toUnsignedByte, + checkInputs, + bytesToBigDecimal, + dataHashesObjToArray, + formatAndConcatenateDataHashes, + formatMrz, + splitToWords +} from '../common/src/utils/utils'; console.log('DEFAULT_PNUMBER', DEFAULT_PNUMBER); console.log('LOCAL_IP', LOCAL_IP); diff --git a/app/metro.config.js b/app/metro.config.js index ad8f87b6d..2dc574587 100644 --- a/app/metro.config.js +++ b/app/metro.config.js @@ -1,4 +1,11 @@ const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); +const path = require('path'); +const extraNodeModules = { + 'common': path.resolve(__dirname + '/../common'), +}; +const watchFolders = [ + path.resolve(__dirname + '/../common') +]; /** * Metro configuration @@ -6,6 +13,11 @@ const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); * * @type {import('metro-config').MetroConfig} */ -const config = {}; +const config = { + resolver: { + extraNodeModules, + }, + watchFolders, +}; module.exports = mergeConfig(getDefaultConfig(__dirname), config); diff --git a/app/package.json b/app/package.json index 8ec90c0d3..94c17a9be 100644 --- a/app/package.json +++ b/app/package.json @@ -39,6 +39,7 @@ "@types/react-native-dotenv": "^0.2.0", "@types/react-test-renderer": "^18.0.0", "babel-jest": "^29.2.1", + "babel-plugin-module-resolver": "^5.0.0", "eslint": "^8.19.0", "jest": "^29.2.1", "metro-react-native-babel-preset": "0.76.7", diff --git a/app/scripts/mrzToSig.ts b/app/scripts/mrzToSig.ts deleted file mode 100644 index ce9e5c52c..000000000 --- a/app/scripts/mrzToSig.ts +++ /dev/null @@ -1,94 +0,0 @@ -import * as crypto from 'crypto'; -import { - arraysAreEqual, - dataHashesObjToArray, - formatMrz, - assembleMrz, - findTimeOfSignature, - parsePubKeyString, - formatAndConcatenateDataHashes, - assembleEContent, -} from '../utils/utils'; -import * as forge from 'node-forge'; -import passportData from '../server/passportData.json'; -import {DataHash} from '../types/passportData'; - -// This script tests the whole flow from MRZ to signature -// The passportData is imported from passportData.json written by the server - -const mrz = passportData.mrz; - -console.log('mrz: ', mrz); - -// Transforms the dataHashes object into an array of arrays -const dataHashes = passportData.dataGroupHashes as DataHash[]; - -const mrzHash = hash(formatMrz(mrz)); - -console.log('mrzHash:', mrzHash); -console.log( - 'mrzHash === dataHashes[0][1] ?', - arraysAreEqual(mrzHash, dataHashes[0][1] as number[]), -); - -const concatenatedDataHashes = formatAndConcatenateDataHashes( - mrzHash, - dataHashes, -); - -const concatenatedDataHashesHashDigest = hash(concatenatedDataHashes); - -// check that concatenatedDataHashesHashDigest is at the right place of passportData.eContent -const sliceOfEContent = passportData.eContent.slice(72, 72 + 32); - -console.log( - 'Are they equal ?', - arraysAreEqual(sliceOfEContent, concatenatedDataHashesHashDigest), -); - -// now let's verify the signature -// const {modulus, exponent} = parsePubKeyString(passportData.publicKey); -// Create the public key -const rsa = forge.pki.rsa; -const publicKey = rsa.setPublicKey( - new forge.jsbn.BigInteger(passportData.modulus, 10), - new forge.jsbn.BigInteger("10001", 16), -); - -// SHA-256 hash of the eContent -const md = forge.md.sha256.create(); -md.update(forge.util.binary.raw.encode(new Uint8Array(passportData.eContent))); -const hashOfEContent = md.digest().getBytes(); - -console.log('modulus', passportData.modulus); -console.log('eContent', bytesToBigDecimal(passportData.eContent)); -console.log('signature', bytesToBigDecimal(passportData.encryptedDigest)); - -// Signature verification -const signatureBytes = Buffer.from(passportData.encryptedDigest).toString( - 'binary', -); -const valid = publicKey.verify(hashOfEContent, signatureBytes); - -if (valid) { - console.log('The signature is valid.'); -} else { - console.log('The signature is not valid.'); -} - -function hash(bytesArray: number[]): number[] { - const hash = crypto.createHash('sha256'); - hash.update(Buffer.from(bytesArray)); - return Array.from(hash.digest()).map(x => (x < 128 ? x : x - 256)); -} -function bytesToBigDecimal(arr: number[]): string { - let result = BigInt(0); - for (let i = 0; i < arr.length; i++) { - result = result * BigInt(256) + BigInt(arr[i] & 0xff); - } - return result.toString(); -} - -function hexToDecimal(hex: string): string { - return BigInt(`0x${hex}`).toString(); -} diff --git a/app/types/passportData.ts b/app/types/passportData.ts deleted file mode 100644 index 2d4d49b92..000000000 --- a/app/types/passportData.ts +++ /dev/null @@ -1,28 +0,0 @@ -export type MrzInfo = { - compositeCheckDigit: string; - dateOfBirth: string; - dateOfBirthCheckDigit: string; - dateOfExpiry: string; - dateOfExpiryCheckDigit: string; - documentCode: string; - documentNumber: string; - documentNumberCheckDigit: string; - documentType: number; - gender: string; - issuingState: string; - nationality: string; - optionalData1: string; - primaryIdentifier: string; - secondaryIdentifier: string; -}; - -export type DataHash = [number, number[]]; - -export type PassportData = { - mrz: string; - signatureAlgorithm: string; - pubKey: {modulus?: string, curveName?: string, publicKeyQ?: string}; - dataGroupHashes: DataHash[]; - eContent: number[]; - encryptedDigest: number[]; -}; diff --git a/app/utils/checks.ts b/app/utils/checks.ts deleted file mode 100644 index e644c50a8..000000000 --- a/app/utils/checks.ts +++ /dev/null @@ -1,23 +0,0 @@ -export function checkInputs( - passportNumber: string, - dateOfBirth: string, - dateOfExpiry: string, -): boolean { - if (passportNumber.length !== 9) { - throw new Error('Passport number must be 9 characters long'); - } - if (dateOfBirth.length !== 6) { - throw new Error('Date of birth must be 6 characters long'); - } - if (dateOfExpiry.length !== 6) { - throw new Error('Date of expiry must be 6 characters long'); - } - return true; -} - -export function getFirstName(mrz: string): string { - const names = mrz.split("<<"); - const firstName = names[1].split("<")[0].trim(); - const capitalized = firstName.charAt(0) + firstName.slice(1).toLowerCase(); - return capitalized || "Unknown"; -} \ No newline at end of file diff --git a/app/utils/computeEContent.ts b/app/utils/computeEContent.ts deleted file mode 100644 index 59623d8b5..000000000 --- a/app/utils/computeEContent.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {PassportData} from '../types/passportData'; -import { - arraysAreEqual, - assembleEContent, - assembleMrz, - findTimeOfSignature, - formatAndConcatenateDataHashes, - formatMrz, -} from './utils'; -import {sha256} from 'js-sha256'; - -// hash logic here because the one in utils.ts only works with node -export function hash(bytesArray: number[]) { - let unsignedBytesArray = bytesArray.map(toUnsignedByte); - let hash = sha256(unsignedBytesArray); - return hexToSignedBytes(hash); -} - -export function hexToSignedBytes(hexString: string) { - let bytes = []; - for (let i = 0; i < hexString.length - 1; i += 2) { - let byte = parseInt(hexString.substr(i, 2), 16); - bytes.push(byte >= 128 ? byte - 256 : byte); - } - return bytes; -} - -export function toUnsignedByte(signedByte: number) { - return signedByte < 0 ? signedByte + 256 : signedByte; -} diff --git a/app/utils/utils.ts b/app/utils/utils.ts index 109194455..b9e92499f 100644 --- a/app/utils/utils.ts +++ b/app/utils/utils.ts @@ -1,200 +1,8 @@ -import {DataHash, MrzInfo, PassportData} from '../types/passportData'; - -export function toUnsigned(byte: number) { - return byte & 0xff; -} - -export function arraysAreEqual(array1: number[], array2: number[]) { - return ( - array1.length === array2.length && - array1.every((value, index) => value === array2[index]) - ); -} - -export function toSigned(byte: number) { - return byte > 127 ? byte - 256 : byte; -} - -export function dataHashesObjToArray(dataHashes: { - [key: string]: number[]; -}): DataHash[] { - return Object.keys(dataHashes) - .map(key => { - const dataHash = dataHashes[key as keyof typeof dataHashes]; - return [Number(key), dataHash]; - }) - .sort((a, b) => (a[0] as number) - (b[0] as number)) as DataHash[]; -} - -export function assembleMrz(mrzInfo: MrzInfo) { - return ( - mrzInfo.documentCode + - '<' + - mrzInfo.issuingState + - mrzInfo.primaryIdentifier + - '<<' + - mrzInfo.secondaryIdentifier + - mrzInfo.documentNumber + - mrzInfo.documentNumberCheckDigit + - mrzInfo.nationality + - mrzInfo.dateOfBirth + - mrzInfo.dateOfBirthCheckDigit + - mrzInfo.gender.substring(0, 1) + - mrzInfo.dateOfExpiry + - mrzInfo.dateOfExpiryCheckDigit + - mrzInfo.optionalData1 + - mrzInfo.compositeCheckDigit - ); -} - -export function formatMrz(mrz: string) { - const mrzCharcodes = [...mrz].map(char => char.charCodeAt(0)); - - // console.log('mrzCharcodes:', mrzCharcodes); - - mrzCharcodes.unshift(88); // the length of the mrz data - mrzCharcodes.unshift(95, 31); // the MRZ_INFO_TAG - mrzCharcodes.unshift(91); // the new length of the whole array - mrzCharcodes.unshift(97); // the tag for DG1 - - // console.log('mrzCharcodes with tags:', mrzCharcodes); - - return mrzCharcodes; -} - -// Example: [49, 15, 23, 13, 49, 57, 49, 50, 49, 54, 49, 55, 50, 50, 51, 56, 90] -// Is "191216172238Z" - 16th December 2019, 17:22:38 UTC -export function findTimeOfSignature(eContentDecomposed: any) { - const timeElement: any = eContentDecomposed.elements.find( - (element: any) => element.elements[0].identifier === '1.2.840.113549.1.9.5', - ); - - if (!timeElement) { - throw new Error('No time element found in eContentDecomposed'); - } - - const timeFound = timeElement.elements[1].elements[0].time; - - // Adding the 4 bytes of the ASN.1 tag and length - // 49 : SET, 15 : LGT, 23 : UTCTIME, 13 : LGT - timeFound.unshift(...[49, 15, 23, 13]); - - return timeFound; -} - -export function parsePubKeyString(pubKeyString: string) { - const modulusMatch = pubKeyString.match(/modulus: ([\w\d]+)\s*public/); - const publicExponentMatch = pubKeyString.match(/public exponent: (\w+)/); - - const modulus = modulusMatch ? modulusMatch[1] : null; - const exponent = publicExponentMatch ? publicExponentMatch[1] : null; - - // console.log('Modulus:', modulus); - // console.log('Public Exponent:', exponent); - - if (!modulus || !exponent) { - throw new Error('Could not parse public key string'); - } - - return { - modulus, - exponent, - }; -} - -export function formatAndConcatenateDataHashes( - mrzHash: number[], - dataHashes: DataHash[], -) { - // Let's replace the first array with the MRZ hash - dataHashes.shift(); - dataHashes.unshift([1, mrzHash]); - - let concat: number[] = [] - - // Starting sequence. Should be the same for everybody, but not sure - concat.push(...[ - 48, -126, 1, 37, 2, 1, 0, 48, 11, 6, 9, 96, -122, 72, 1, 101, 3, 4, 2, 1, - 48, -126, 1, 17, - ]) - - for(const dataHash of dataHashes) { - concat.push(...[48, 37, 2, 1, dataHash[0], 4, 32, ...dataHash[1]]) - } - - return concat; -} - -export function assembleEContent( - messageDigest: number[], - timeOfSignature: number[], -) { - const constructedEContent = []; - - // Detailed description is in private file r&d.ts for now - // First, the tag and length, assumed to be always the same - constructedEContent.push(...[49, 102]); - - // 1.2.840.113549.1.9.3 is RFC_3369_CONTENT_TYPE_OID - constructedEContent.push( - ...[48, 21, 6, 9, 42, -122, 72, -122, -9, 13, 1, 9, 3], - ); - // 2.23.136.1.1.1 is ldsSecurityObject - constructedEContent.push(...[49, 8, 6, 6, 103, -127, 8, 1, 1, 1]); - - // 1.2.840.113549.1.9.5 is signing-time - constructedEContent.push( - ...[48, 28, 6, 9, 42, -122, 72, -122, -9, 13, 1, 9, 5], - ); - // time of the signature - constructedEContent.push(...timeOfSignature); - // 1.2.840.113549.1.9.4 is RFC_3369_MESSAGE_DIGEST_OID - constructedEContent.push( - ...[48, 47, 6, 9, 42, -122, 72, -122, -9, 13, 1, 9, 4], - ); - // TAG and length of the message digest - constructedEContent.push(...[49, 34, 4, 32]); - - constructedEContent.push(...messageDigest); - return constructedEContent; -} - -export const toBinaryString = (byte: any) => { - const binary = (parseInt(byte, 10) & 0xFF).toString(2).padStart(8, '0'); - return binary; -}; - -export function splitToWords( - number: bigint, - wordsize: bigint, - numberElement: bigint -) { - let t = number - const words: string[] = [] - for (let i = BigInt(0); i < numberElement; ++i) { - const baseTwo = BigInt(2) - - words.push(`${t % BigInt(Math.pow(Number(baseTwo), Number(wordsize)))}`) - t = BigInt(t / BigInt(Math.pow(Number(BigInt(2)), Number(wordsize)))) - } - if (!(t == BigInt(0))) { - throw `Number ${number} does not fit in ${( - wordsize * numberElement - ).toString()} bits` - } - return words -} - -export function bytesToBigDecimal(arr: number[]): string { - let result = BigInt(0); - for (let i = 0; i < arr.length; i++) { - result = result * BigInt(256) + BigInt(arr[i] & 0xff); - } - return result.toString(); -} - -export function hexToDecimal(hex: string): string { - return BigInt(`0x${hex}`).toString(); +export function getFirstName(mrz: string): string { + const names = mrz.split("<<"); + const firstName = names[1].split("<")[0].trim(); + const capitalized = firstName.charAt(0) + firstName.slice(1).toLowerCase(); + return capitalized || "Unknown"; } export function formatDuration(durationInMs: number) { diff --git a/app/yarn.lock b/app/yarn.lock index d93f84246..c47456fcb 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -3604,6 +3604,17 @@ babel-plugin-jest-hoist@^29.5.0: "@types/babel__core" "^7.1.14" "@types/babel__traverse" "^7.0.6" +babel-plugin-module-resolver@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.0.tgz#2b7fc176bd55da25f516abf96015617b4f70fc73" + integrity sha512-g0u+/ChLSJ5+PzYwLwP8Rp8Rcfowz58TJNCe+L/ui4rpzE/mg//JVX0EWBUYoxaextqnwuGHzfGp2hh0PPV25Q== + dependencies: + find-babel-config "^2.0.0" + glob "^8.0.3" + pkg-up "^3.1.0" + reselect "^4.1.7" + resolve "^1.22.1" + babel-plugin-polyfill-corejs2@^0.4.4: version "0.4.5" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c" @@ -3767,6 +3778,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -4829,6 +4847,14 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +find-babel-config@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-2.0.0.tgz#a8216f825415a839d0f23f4d18338a1cc966f701" + integrity sha512-dOKT7jvF3hGzlW60Gc3ONox/0rRZ/tz7WCil0bqA1In/3I8f1BctpXahRnEKDySZqci7u+dqq93sZST9fOJpFw== + dependencies: + json5 "^2.1.1" + path-exists "^4.0.0" + find-cache-dir@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" @@ -4930,6 +4956,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -5009,6 +5040,17 @@ glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.3: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -5103,6 +5145,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + hermes-estree@0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.12.0.tgz#8a289f9aee854854422345e6995a48613bac2ca8" @@ -5299,6 +5348,13 @@ is-core-module@^2.11.0, is-core-module@^2.9.0: dependencies: has "^1.0.3" +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + is-date-object@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" @@ -5977,7 +6033,7 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json5@^2.2.2: +json5@^2.1.1, json5@^2.2.2: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -6479,6 +6535,13 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatc dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -6870,6 +6933,13 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pkg-up@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" + integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== + dependencies: + find-up "^3.0.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -7248,6 +7318,11 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +reselect@^4.1.7: + version "4.1.8" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" + integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -7284,6 +7359,15 @@ resolve@^1.14.2, resolve@^1.20.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.4: version "2.0.0-next.4" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660" diff --git a/circuits/scripts/mrzToSig.ts b/circuits/scripts/mrzToSig.ts index 48d1d684f..32ccb7347 100644 --- a/circuits/scripts/mrzToSig.ts +++ b/circuits/scripts/mrzToSig.ts @@ -1,20 +1,13 @@ import * as crypto from 'crypto'; import { arraysAreEqual, - dataHashesObjToArray, formatMrz, - assembleMrz, - findTimeOfSignature, - parsePubKeyString, formatAndConcatenateDataHashes, - assembleEContent, - hexToDecimal, bytesToBigDecimal, -} from '../utils/utils'; +} from '../../common/src/utils/utils'; import * as forge from 'node-forge'; -import passportData from '../inputs/passportData_florent.json'; -import {DataHash} from '../utils/types'; -import { genSampleData } from '../utils/sampleData'; +import passportData from '../../common/inputs/passportData.json'; +import {DataHash} from '../../common/src/utils/types'; // This script tests the whole flow from MRZ to signature // The passportData is imported from passportData.json written by the server @@ -54,7 +47,7 @@ console.log( // Create the public key const rsa = forge.pki.rsa; const publicKey = rsa.setPublicKey( - new forge.jsbn.BigInteger(passportData.modulus, 10), + new forge.jsbn.BigInteger(passportData.pubKey.modulus, 10), new forge.jsbn.BigInteger("10001", 16), ); @@ -63,7 +56,7 @@ const md = forge.md.sha256.create(); md.update(forge.util.binary.raw.encode(new Uint8Array(passportData.eContent))); const hashOfEContent = md.digest().getBytes(); -console.log('modulus', passportData.modulus); +console.log('modulus', passportData.pubKey.modulus); console.log('eContent', bytesToBigDecimal(passportData.eContent)); console.log('signature', bytesToBigDecimal(passportData.encryptedDigest)); diff --git a/circuits/test/passport.test.ts b/circuits/test/passport.test.ts index 52dadf40c..4c018d252 100644 --- a/circuits/test/passport.test.ts +++ b/circuits/test/passport.test.ts @@ -1,11 +1,11 @@ import { describe } from 'mocha' import chai, { assert, expect } from 'chai' import chaiAsPromised from 'chai-as-promised' -import { arraysAreEqual, bytesToBigDecimal, formatAndConcatenateDataHashes, formatMrz, splitToWords } from '../utils/utils' +import { hash, toUnsignedByte, arraysAreEqual, bytesToBigDecimal, formatAndConcatenateDataHashes, formatMrz, splitToWords } from '../../common/src/utils/utils' import { groth16 } from 'snarkjs' -import { hash, toUnsignedByte } from '../utils/computeEContent' -import { DataHash, PassportData } from '../utils/types' -import { genSampleData } from '../utils/sampleData' +import { DataHash, PassportData } from '../../common/src/utils/types' +import { getPassportData } from '../../common/src/utils/passportData' +import { attributeToPosition } from '../../common/src/constants/constants' const fs = require('fs'); chai.use(chaiAsPromised) @@ -17,15 +17,7 @@ describe('Circuit tests', function () { let inputs: any; this.beforeAll(async () => { - if (fs.existsSync('inputs/passportData.json')) { - passportData = require('../inputs/passportData.json'); - } else { - passportData = (await genSampleData()) as PassportData; - if (!fs.existsSync("inputs/")) { - fs.mkdirSync("inputs/"); - } - fs.writeFileSync('inputs/passportData.json', JSON.stringify(passportData)); - } + const passportData = getPassportData(); const formattedMrz = formatMrz(passportData.mrz); const mrzHash = hash(formatMrz(passportData.mrz)); @@ -51,7 +43,7 @@ describe('Circuit tests', function () { dataHashes: concatenatedDataHashes.map(toUnsignedByte).map(byte => String(byte)), eContentBytes: passportData.eContent.map(toUnsignedByte).map(byte => String(byte)), pubkey: splitToWords( - BigInt(passportData.modulus), + BigInt(passportData.pubKey.modulus), BigInt(64), BigInt(32) ), @@ -60,7 +52,7 @@ describe('Circuit tests', function () { BigInt(64), BigInt(32) ), - address: "0x9D392187c08fc28A86e1354aD63C70897165b982", + address: "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", } }) @@ -133,16 +125,6 @@ describe('Circuit tests', function () { }) describe('Selective disclosure', function() { - const attributeToPosition = { - issuing_state: [2, 4], - name: [5, 43], - passport_number: [44, 52], - nationality: [54, 56], - date_of_birth: [57, 62], - gender: [64, 64], - expiry_date: [65, 70], - } - const attributeCombinations = [ ['issuing_state', 'name'], ['passport_number', 'nationality', 'date_of_birth'], @@ -150,7 +132,7 @@ describe('Circuit tests', function () { ]; attributeCombinations.forEach(combination => { - it.only(`Disclosing ${combination.join(", ")}`, async function () { + it(`Disclosing ${combination.join(", ")}`, async function () { const attributeToReveal = Object.keys(attributeToPosition).reduce((acc, attribute) => { acc[attribute] = combination.includes(attribute); return acc; @@ -224,21 +206,3 @@ describe('Circuit tests', function () { }) - -// [ -// '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', -// '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', -// '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', -// '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', -// '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', -// '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', -// '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', -// '\x00', '\x00', '\x00', '\x00', '\x00', 'F', 'R', -// 'A', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', -// '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', -// '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', -// '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', -// '\x00', '\x00', '\x00', '\x00' -// ] - -// P= 128 ? byte - 256 : byte); - } - return bytes; -} - -export function toUnsignedByte(signedByte: number) { - return signedByte < 0 ? signedByte + 256 : signedByte; -} diff --git a/circuits/utils/types.ts b/circuits/utils/types.ts deleted file mode 100644 index 53c9c0414..000000000 --- a/circuits/utils/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -export type MrzInfo = { - compositeCheckDigit: string; - dateOfBirth: string; - dateOfBirthCheckDigit: string; - dateOfExpiry: string; - dateOfExpiryCheckDigit: string; - documentCode: string; - documentNumber: string; - documentNumberCheckDigit: string; - documentType: number; - gender: string; - issuingState: string; - nationality: string; - optionalData1: string; - primaryIdentifier: string; - secondaryIdentifier: string; -}; - -export type DataHash = [number, number[]]; - -export type PassportData = { - mrz: string; - modulus: string; - dataGroupHashes: DataHash[]; - eContent: number[]; - encryptedDigest: number[]; -}; diff --git a/common/.gitignore b/common/.gitignore new file mode 100644 index 000000000..281d0e41e --- /dev/null +++ b/common/.gitignore @@ -0,0 +1,2 @@ +inputs +node_modules/ \ No newline at end of file diff --git a/common/README.md b/common/README.md new file mode 100644 index 000000000..218bda03b --- /dev/null +++ b/common/README.md @@ -0,0 +1,3 @@ +# Proof of Passport Commons + +Constants and utils used in multiple Proof of Passport subdirectories. diff --git a/common/package.json b/common/package.json new file mode 100644 index 000000000..5de125cdf --- /dev/null +++ b/common/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "@babel/runtime": "^7.23.4", + "js-sha256": "^0.10.1", + "node-forge": "^1.3.1" + }, + "devDependencies": { + "@types/node-forge": "^1.3.10" + } +} diff --git a/common/src/constants/constants.ts b/common/src/constants/constants.ts new file mode 100644 index 000000000..3f6743445 --- /dev/null +++ b/common/src/constants/constants.ts @@ -0,0 +1,9 @@ +export const attributeToPosition = { + issuing_state: [2, 4], + name: [5, 43], + passport_number: [44, 52], + nationality: [54, 56], + date_of_birth: [57, 62], + gender: [64, 64], + expiry_date: [65, 70], +} \ No newline at end of file diff --git a/circuits/utils/sampleData.ts b/common/src/utils/passportData.ts similarity index 60% rename from circuits/utils/sampleData.ts rename to common/src/utils/passportData.ts index 12d5008e9..a9b3854d1 100644 --- a/circuits/utils/sampleData.ts +++ b/common/src/utils/passportData.ts @@ -1,7 +1,7 @@ -import { hash } from "./computeEContent"; -import { DataHash } from "./types"; -import { assembleEContent, formatAndConcatenateDataHashes, formatMrz, hexToDecimal } from "./utils"; +import { DataHash, PassportData } from "./types"; +import { hash, assembleEContent, formatAndConcatenateDataHashes, formatMrz, hexToDecimal } from "./utils"; import * as forge from 'node-forge'; +const fs = require('fs'); const sampleMRZ = "P c.charCodeAt(0)); + const signatureBytes = Array.from(signature, (c: string) => c.charCodeAt(0)); // Signature verification - const hashOfEContent = md.digest().getBytes(); - const publicKey = rsa.setPublicKey( - new forge.jsbn.BigInteger(modulus, 16), - new forge.jsbn.BigInteger("10001", 16), - ); - const valid = publicKey.verify(hashOfEContent, signature); - console.log('valid ?', valid) + // const hashOfEContent = md.digest().getBytes(); + // const publicKey = rsa.setPublicKey( + // new forge.jsbn.BigInteger(modulus, 16), + // new forge.jsbn.BigInteger("10001", 16), + // ); + // const valid = publicKey.verify(hashOfEContent, signature); + // console.log('valid ?', valid) return { - "mrz": sampleMRZ, - modulus: hexToDecimal(modulus), - "dataGroupHashes": sampleDataHashes, - "eContent": eContent, - "encryptedDigest": signatureBytes, + mrz: sampleMRZ, + signatureAlgorithm: 'SHA256withRSA', // sha256WithRSAEncryption + pubKey: { + modulus: hexToDecimal(modulus), + }, + dataGroupHashes: sampleDataHashes as DataHash[], + eContent: eContent, + encryptedDigest: signatureBytes, } } - +export function getPassportData(): PassportData { + if (fs.existsSync('../../inputs/passportData.json')) { + return require('../../inputs/passportData.json'); + } else { + const sampleData = genSampleData(); + if (!fs.existsSync('../../inputs/')) { + fs.mkdirSync('../../inputs/'); + } + fs.writeFileSync('../../inputs/passportData.json', JSON.stringify(sampleData)); + return sampleData; + } +} diff --git a/common/src/utils/types.ts b/common/src/utils/types.ts new file mode 100644 index 000000000..7fb011bf3 --- /dev/null +++ b/common/src/utils/types.ts @@ -0,0 +1,10 @@ +export type DataHash = [number, number[]]; + +export type PassportData = { + mrz: string; + signatureAlgorithm: string; + pubKey: {modulus?: string, curveName?: string, publicKeyQ?: string}; + dataGroupHashes: DataHash[]; + eContent: number[]; + encryptedDigest: number[]; +}; diff --git a/circuits/utils/utils.ts b/common/src/utils/utils.ts similarity index 77% rename from circuits/utils/utils.ts rename to common/src/utils/utils.ts index f5613885c..a7a4de71f 100644 --- a/circuits/utils/utils.ts +++ b/common/src/utils/utils.ts @@ -1,19 +1,5 @@ -import {DataHash, MrzInfo, PassportData} from './types'; - -export function toUnsigned(byte: number) { - return byte & 0xff; -} - -export function arraysAreEqual(array1: number[], array2: number[]) { - return ( - array1.length === array2.length && - array1.every((value, index) => value === array2[index]) - ); -} - -export function toSigned(byte: number) { - return byte > 127 ? byte - 256 : byte; -} +import { DataHash } from './types'; +import {sha256} from 'js-sha256'; export function dataHashesObjToArray(dataHashes: { [key: string]: number[]; @@ -26,27 +12,6 @@ export function dataHashesObjToArray(dataHashes: { .sort((a, b) => (a[0] as number) - (b[0] as number)) as DataHash[]; } -export function assembleMrz(mrzInfo: MrzInfo) { - return ( - mrzInfo.documentCode + - '<' + - mrzInfo.issuingState + - mrzInfo.primaryIdentifier + - '<<' + - mrzInfo.secondaryIdentifier + - mrzInfo.documentNumber + - mrzInfo.documentNumberCheckDigit + - mrzInfo.nationality + - mrzInfo.dateOfBirth + - mrzInfo.dateOfBirthCheckDigit + - mrzInfo.gender.substring(0, 1) + - mrzInfo.dateOfExpiry + - mrzInfo.dateOfExpiryCheckDigit + - mrzInfo.optionalData1 + - mrzInfo.compositeCheckDigit - ); -} - export function formatMrz(mrz: string) { const mrzCharcodes = [...mrz].map(char => char.charCodeAt(0)); @@ -62,26 +27,6 @@ export function formatMrz(mrz: string) { return mrzCharcodes; } -// Example: [49, 15, 23, 13, 49, 57, 49, 50, 49, 54, 49, 55, 50, 50, 51, 56, 90] -// Is "191216172238Z" - 16th December 2019, 17:22:38 UTC -export function findTimeOfSignature(eContentDecomposed: any) { - const timeElement: any = eContentDecomposed.elements.find( - (element: any) => element.elements[0].identifier === '1.2.840.113549.1.9.5', - ); - - if (!timeElement) { - throw new Error('No time element found in eContentDecomposed'); - } - - const timeFound = timeElement.elements[1].elements[0].time; - - // Adding the 4 bytes of the ASN.1 tag and length - // 49 : SET, 15 : LGT, 23 : UTCTIME, 13 : LGT - timeFound.unshift(...[49, 15, 23, 13]); - - return timeFound; -} - export function parsePubKeyString(pubKeyString: string) { const modulusMatch = pubKeyString.match(/modulus: ([\w\d]+)\s*public/); const publicExponentMatch = pubKeyString.match(/public exponent: (\w+)/); @@ -160,6 +105,38 @@ export function assembleEContent( return constructedEContent; } +export function checkInputs( + passportNumber: string, + dateOfBirth: string, + dateOfExpiry: string, +): boolean { + if (passportNumber.length !== 9) { + throw new Error('Passport number must be 9 characters long'); + } + if (dateOfBirth.length !== 6) { + throw new Error('Date of birth must be 6 characters long'); + } + if (dateOfExpiry.length !== 6) { + throw new Error('Date of expiry must be 6 characters long'); + } + return true; +} + +export function toUnsigned(byte: number) { + return byte & 0xff; +} + +export function arraysAreEqual(array1: number[], array2: number[]) { + return ( + array1.length === array2.length && + array1.every((value, index) => value === array2[index]) + ); +} + +export function toSigned(byte: number) { + return byte > 127 ? byte - 256 : byte; +} + export const toBinaryString = (byte: any) => { const binary = (parseInt(byte, 10) & 0xFF).toString(2).padStart(8, '0'); return binary; @@ -196,4 +173,24 @@ export function bytesToBigDecimal(arr: number[]): string { export function hexToDecimal(hex: string): string { return BigInt(`0x${hex}`).toString(); -} \ No newline at end of file +} + +// hash logic here because the one in utils.ts only works with node +export function hash(bytesArray: number[]) { + let unsignedBytesArray = bytesArray.map(toUnsignedByte); + let hash = sha256(unsignedBytesArray); + return hexToSignedBytes(hash); +} + +export function hexToSignedBytes(hexString: string): number[] { + let bytes = []; + for (let i = 0; i < hexString.length - 1; i += 2) { + let byte = parseInt(hexString.substr(i, 2), 16); + bytes.push(byte >= 128 ? byte - 256 : byte); + } + return bytes; +} + +export function toUnsignedByte(signedByte: number) { + return signedByte < 0 ? signedByte + 256 : signedByte; +} diff --git a/common/tsconfig.json b/common/tsconfig.json new file mode 100644 index 000000000..a05d480d5 --- /dev/null +++ b/common/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "resolveJsonModule": true, + "esModuleInterop": true, + "target": "ES2015", + "moduleResolution": "node" + } +} \ No newline at end of file diff --git a/common/yarn.lock b/common/yarn.lock new file mode 100644 index 000000000..64ca82b09 --- /dev/null +++ b/common/yarn.lock @@ -0,0 +1,44 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/runtime@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.4.tgz#36fa1d2b36db873d25ec631dcc4923fdc1cf2e2e" + integrity sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg== + dependencies: + regenerator-runtime "^0.14.0" + +"@types/node-forge@^1.3.10": + version "1.3.10" + resolved "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz" + integrity sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "20.10.0" + resolved "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz" + integrity sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ== + dependencies: + undici-types "~5.26.4" + +js-sha256@^0.10.1: + version "0.10.1" + resolved "https://registry.npmjs.org/js-sha256/-/js-sha256-0.10.1.tgz" + integrity sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw== + +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==