refactor utils, inputs and constants into /common

This commit is contained in:
0xturboblitz
2023-11-25 11:12:21 +01:00
parent 2abdf9116a
commit c7985fa8e1
23 changed files with 302 additions and 579 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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",

View File

@@ -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();
}

View File

@@ -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[];
};

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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"

View File

@@ -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));

View File

@@ -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<FRADUPONT<<ALPHONSE<HUGUES<ALBERT<<<<<<<<<24HB818324FRA0402111M3111115<<<<<<<<<<<<<<02

View File

@@ -1,21 +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(mrzInfo: any): string {
const firstName = mrzInfo.secondaryIdentifier.split('<')[0];
return firstName.charAt(0).toUpperCase() + firstName.slice(1).toLowerCase();
}

View File

@@ -1,21 +0,0 @@
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): 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;
}

View File

@@ -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[];
};

2
common/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
inputs
node_modules/

3
common/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Proof of Passport Commons
Constants and utils used in multiple Proof of Passport subdirectories.

10
common/package.json Normal file
View File

@@ -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"
}
}

View File

@@ -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],
}

View File

@@ -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<FRADUPONT<<ALPHONSE<HUGUES<ALBERT<<<<<<<<<24HB818324FRA0402111M3111115<<<<<<<<<<<<<<02"
const sampleDataHashes = [
@@ -32,7 +32,7 @@ const sampleDataHashes = [
]
const sampleTimeOfSig = [49, 15, 23, 13, 49, 57, 49, 50, 49, 54, 49, 55, 50, 50, 51, 56, 90]
export async function genSampleData() {
export function genSampleData(): PassportData {
const mrzHash = hash(formatMrz(sampleMRZ));
sampleDataHashes.unshift([1, mrzHash]);
const concatenatedDataHashes = formatAndConcatenateDataHashes(
@@ -52,24 +52,38 @@ export async function genSampleData() {
md.update(forge.util.binary.raw.encode(new Uint8Array(eContent)));
const signature = privKey.sign(md)
const signatureBytes = Array.from(signature, c => 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;
}
}

10
common/src/utils/types.ts Normal file
View File

@@ -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[];
};

View File

@@ -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();
}
}
// 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;
}

8
common/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true,
"target": "ES2015",
"moduleResolution": "node"
}
}

44
common/yarn.lock Normal file
View File

@@ -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==