diff --git a/app/App.tsx b/app/App.tsx index 399f23578..82b6d9da4 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -1,18 +1,20 @@ import React, { useEffect } from 'react'; +import "react-native-get-random-values" import "@ethersproject/shims" import MainScreen from './src/screens/MainScreen'; import { Buffer } from 'buffer'; import { YStack } from 'tamagui'; import { useToastController } from '@tamagui/toast'; -import { downloadZkey } from './src/utils/zkeyDownload'; import useNavigationStore from './src/stores/navigationStore'; import { AMPLITUDE_KEY } from '@env'; import * as amplitude from '@amplitude/analytics-react-native'; +import useUserStore from './src/stores/userStore'; global.Buffer = Buffer; function App(): JSX.Element { const toast = useToastController(); const setToast = useNavigationStore((state) => state.setToast); + const initUserStore = useUserStore((state) => state.initUserStore); useEffect(() => { setToast(toast); @@ -20,10 +22,7 @@ function App(): JSX.Element { useEffect(() => { amplitude.init(AMPLITUDE_KEY); - - // downloadZkey("register_sha256WithRSAEncryption_65537"); // might move after nfc scanning - // downloadZkey("disclose"); - downloadZkey("proof_of_passport"); + initUserStore(); }, []); // TODO: when passportData already stored, retrieve and jump to main screen diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 3750b9896..fcab64cf5 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -291,6 +291,8 @@ PODS: - React-jsinspector (0.72.3) - React-logger (0.72.3): - glog + - react-native-get-random-values (1.11.0): + - React-Core - react-native-netinfo (11.3.1): - React-Core - React-NativeModulesApple (0.72.3): @@ -398,12 +400,12 @@ PODS: - React-jsi (= 0.72.3) - React-logger (= 0.72.3) - React-perflogger (= 0.72.3) - - RNCAsyncStorage (1.23.1): - - React-Core - RNCClipboard (1.5.1): - React-Core - RNFS (2.20.0): - React-Core + - RNKeychain (8.2.0): + - React-Core - RNSVG (13.4.0): - React-Core - RNZipArchive (6.1.0): @@ -443,6 +445,7 @@ DEPENDENCIES: - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) @@ -461,9 +464,9 @@ DEPENDENCIES: - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCClipboard (from `../node_modules/@react-native-community/clipboard`)" - RNFS (from `../node_modules/react-native-fs`) + - RNKeychain (from `../node_modules/react-native-keychain`) - RNSVG (from `../node_modules/react-native-svg`) - RNZipArchive (from `../node_modules/react-native-zip-archive`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -524,6 +527,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/jsinspector" React-logger: :path: "../node_modules/react-native/ReactCommon/logger" + react-native-get-random-values: + :path: "../node_modules/react-native-get-random-values" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" React-NativeModulesApple: @@ -560,12 +565,12 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/react/utils" ReactCommon: :path: "../node_modules/react-native/ReactCommon" - RNCAsyncStorage: - :path: "../node_modules/@react-native-async-storage/async-storage" RNCClipboard: :path: "../node_modules/@react-native-community/clipboard" RNFS: :path: "../node_modules/react-native-fs" + RNKeychain: + :path: "../node_modules/react-native-keychain" RNSVG: :path: "../node_modules/react-native-svg" RNZipArchive: @@ -605,6 +610,7 @@ SPEC CHECKSUMS: React-jsiexecutor: 2c15ba1bace70177492368d5180b564f165870fd React-jsinspector: b511447170f561157547bc0bef3f169663860be7 React-logger: c5b527272d5f22eaa09bb3c3a690fee8f237ae95 + react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 react-native-netinfo: bdb108d340cdb41875c9ced535977cac6d2ff321 React-NativeModulesApple: 0438665fc7473be6edc496e823e6ea0b0537b46c React-perflogger: 6bd153e776e6beed54c56b0847e1220a3ff92ba5 @@ -623,9 +629,9 @@ SPEC CHECKSUMS: React-runtimescheduler: ec1066a4f2d1152eb1bc3fb61d69376b3bc0dde0 React-utils: d55ba834beb39f01b0b470ae43478c0a3a024abe ReactCommon: 68e3a815fbb69af3bb4196e04c6ae7abb306e7a8 - RNCAsyncStorage: 826b603ae9c0f88b5ac4e956801f755109fa4d5c RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 + RNKeychain: bfe3d12bf4620fe488771c414530bf16e88f3678 RNSVG: 07dbd870b0dcdecc99b3a202fa37c8ca163caec2 RNZipArchive: ef9451b849c45a29509bf44e65b788829ab07801 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 diff --git a/app/ios/ProofOfPassport/Info.plist b/app/ios/ProofOfPassport/Info.plist index 87c2f53b8..08aba7a5c 100644 --- a/app/ios/ProofOfPassport/Info.plist +++ b/app/ios/ProofOfPassport/Info.plist @@ -31,9 +31,11 @@ NFCReaderUsageDescription Need NFC to read Passport NSAppTransportSecurity - + + NSFaceIDUsageDescription + Needed to secure the secret NSCameraUsageDescription - Needed to scan your passport MRZ, you can however enter it manually. + Needed to scan the passport MRZ. NSHumanReadableCopyright NSLocationWhenInUseUsageDescription diff --git a/app/package.json b/app/package.json index bac2e8860..7609a7cc4 100644 --- a/app/package.json +++ b/app/package.json @@ -13,7 +13,6 @@ "@amplitude/analytics-react-native": "^1.4.7", "@babel/plugin-transform-private-methods": "^7.23.3", "@ethersproject/shims": "^5.7.0", - "@react-native-async-storage/async-storage": "^1.23.1", "@react-native-community/clipboard": "^1.5.1", "@react-native-community/netinfo": "^11.3.1", "@tamagui/colors": "^1.94.3", @@ -40,6 +39,8 @@ "react-native": "0.72.3", "react-native-canvas": "^0.1.39", "react-native-fs": "^2.20.0", + "react-native-get-random-values": "^1.11.0", + "react-native-keychain": "^8.2.0", "react-native-passport-reader": "^1.0.3", "react-native-svg": "13.4.0", "react-native-zip-archive": "^6.1.0", diff --git a/app/src/apps/sbt.tsx b/app/src/apps/sbt.tsx index f1c19882c..cc83debdb 100644 --- a/app/src/apps/sbt.tsx +++ b/app/src/apps/sbt.tsx @@ -128,17 +128,8 @@ export const sbtApp: AppType = { { developmentMode: false } ); - // remove that when it's only disclosure proof - amplitude.track(`Sig alg supported: ${passportData.signatureAlgorithm}`); - - Object.keys(inputs).forEach((key) => { - if (Array.isArray(inputs[key as keyof typeof inputs])) { - console.log(key, inputs[key as keyof typeof inputs].slice(0, 10), '...'); - } else { - console.log(key, inputs[key as keyof typeof inputs]); - } - }); - + console.log('inputs:', inputs); + const start = Date.now(); const proof = await generateProof( diff --git a/app/src/screens/MainScreen.tsx b/app/src/screens/MainScreen.tsx index 2a6b03579..0dae8a908 100644 --- a/app/src/screens/MainScreen.tsx +++ b/app/src/screens/MainScreen.tsx @@ -31,7 +31,9 @@ const MainScreen: React.FC = () => { dateOfBirth, dateOfExpiry, deleteMrzFields, - update + update, + clearPassportDataFromStorage, + clearSecretFromStorage, } = useUserStore() const { @@ -73,6 +75,7 @@ const MainScreen: React.FC = () => { scan(); } } + useEffect(() => { if (passportNumber?.length === 9 && (dateOfBirth?.length === 6 && dateOfExpiry?.length === 6)) { setStep(Steps.MRZ_SCAN_COMPLETED); @@ -251,6 +254,25 @@ const MainScreen: React.FC = () => { + +
+ + +
+ +
+ + +
+ diff --git a/app/src/screens/ProveScreen.tsx b/app/src/screens/ProveScreen.tsx index fea53083e..1c76e8553 100644 --- a/app/src/screens/ProveScreen.tsx +++ b/app/src/screens/ProveScreen.tsx @@ -40,7 +40,12 @@ const ProveScreen: React.FC = () => { majority, disclosure, update - } = useAppStore() ; + } = useAppStore(); + + const { + registered, + passportData, + } = useUserStore(); const handleDisclosureChange = (field: string) => { const requiredOrOptional = selectedApp.disclosureOptions[field as keyof typeof selectedApp.disclosureOptions]; @@ -58,7 +63,6 @@ const ProveScreen: React.FC = () => { }; const { height } = useWindowDimensions(); - const passportData = useUserStore(state => state.passportData); useEffect(() => { // this already checks if downloading is required @@ -210,7 +214,14 @@ const ProveScreen: React.FC = () => { backgroundColor={address == ethers.ZeroAddress ? "#cecece" : "#3185FC"} alignSelf='center' > - {isZkeyDownloading[selectedApp.circuit] ? ( + {!registered ? ( + + + + Registering identity... + + + ) : isZkeyDownloading[selectedApp.circuit] ? ( diff --git a/app/src/stores/userStore.ts b/app/src/stores/userStore.ts index e56d48ee9..558fa6c97 100644 --- a/app/src/stores/userStore.ts +++ b/app/src/stores/userStore.ts @@ -6,12 +6,25 @@ import { } from '@env'; import { mockPassportData_sha256WithRSAEncryption_65537 } from '../../../common/src/utils/mockPassportData'; import { PassportData } from '../../../common/src/utils/types'; +import * as Keychain from 'react-native-keychain'; +import * as amplitude from '@amplitude/analytics-react-native'; +import useNavigationStore from './navigationStore'; +import { Steps } from '../utils/utils'; +import { ethers } from 'ethers'; +import { downloadZkey } from '../utils/zkeyDownload'; interface UserState { passportNumber: string dateOfBirth: string dateOfExpiry: string + registered: boolean passportData: PassportData + secret: string + initUserStore: () => void + registerPassportData: (passportData: PassportData) => void + registerCommitment: (secret: string, passportData: PassportData) => void + clearPassportDataFromStorage: () => void + clearSecretFromStorage: () => void update: (patch: any) => void deleteMrzFields: () => void } @@ -21,7 +34,138 @@ const useUserStore = create((set, get) => ({ dateOfBirth: DEFAULT_DOB ?? "", dateOfExpiry: DEFAULT_DOE ?? "", + registered: false, passportData: mockPassportData_sha256WithRSAEncryption_65537, + secret: "", + + // When user opens the app, checks presence of passportData + // - If passportData is not present, starts the onboarding flow + // - If passportData is present, then secret must be here too (they are always set together). Request the tree. + // - If the commitment is present in the tree, proceed to main screen + // - If the commitment is not present in the tree, proceed to main screen AND try registering it in the background + initUserStore: async () => { + const passportDataCreds = await Keychain.getGenericPassword({ service: "passportData" }); + if (!passportDataCreds) { + console.log("No passport data found, starting onboarding flow") + return; + } + const secretCreds = await Keychain.getGenericPassword({ service: "secret" }) + + const secret = (secretCreds as Keychain.UserCredentials).password + + set({ + passportData: JSON.parse(passportDataCreds.password), + secret, + }); + useNavigationStore.getState().setStep(Steps.NFC_SCAN_COMPLETED); // this currently means go to app selection screen + + // download zkeys if they are not already downloaded + // downloadZkey("register_sha256WithRSAEncryption_65537"); // might move after nfc scanning + // downloadZkey("disclose"); + downloadZkey("proof_of_passport"); + + // TODO: check if the commitment is already registered, if not retry registering it + + // set({ + // registered: true, + // }); + }, + + // When reading passport for the first time: + // - Check presence of secret. If there is none, create one and store it + // - Store the passportData and try registering the commitment in the background + registerPassportData: async (passportData) => { + const secretCreds = await Keychain.getGenericPassword({ service: "secret" }); + + if (secretCreds && secretCreds.password) { + // This should only ever happen if the user deletes the passport data in the options + console.log("secret is already registered, let's keep it.") + } else { + const randomWallet = ethers.Wallet.createRandom(); + const secret = randomWallet.privateKey; + await Keychain.setGenericPassword("secret", secret, { service: "secret" }); + } + + const newSecretCreds = await Keychain.getGenericPassword({ service: "secret" }) + const secret = (newSecretCreds as Keychain.UserCredentials).password + + const passportDataCreds = await Keychain.getGenericPassword({ service: "passportData" }); + + if (passportDataCreds && passportDataCreds.password) { + throw new Error("passportData is already registered, this should never happen") + } + + await Keychain.setGenericPassword("passportData", JSON.stringify(passportData), { service: "passportData" }); + + get().registerCommitment( + secret, + passportData + ) + + set({ + passportData, + secret + }); + }, + + registerCommitment: async (secret, passportData) => { + // just like in handleProve, generate inputs and launch commitment registration + const { + toast + } = useNavigationStore.getState(); + + try { + // const inputs = generateCircuitInputsRegister( + // passportData, + // secret, + // { developmentMode: false } + // ); + + // amplitude.track(`Sig alg supported: ${passportData.signatureAlgorithm}`); + + // Object.keys(inputs).forEach((key) => { + // if (Array.isArray(inputs[key as keyof typeof inputs])) { + // console.log(key, inputs[key as keyof typeof inputs].slice(0, 10), '...'); + // } else { + // console.log(key, inputs[key as keyof typeof inputs]); + // } + // }); + + // const start = Date.now(); + + // const proof = await generateProof( + // `Register_${passportData.signatureAlgorithm}`, // TODO format it + // inputs, + // ); + + // const end = Date.now(); + // console.log('Total proof time from frontend:', end - start); + // amplitude.track('Proof generation successful, took ' + ((end - start) / 1000) + ' seconds'); + + // // TODO send the proof to the relayer + + // set({ + // registered: true, + // }); + } catch (error: any) { + console.error(error); + toast?.show('Error', { + message: "Error registering your identity, please relaunch the app", + customData: { + type: "error", + }, + }) + amplitude.track(error.message); + } + }, + + clearPassportDataFromStorage: async () => { + await Keychain.resetGenericPassword({ service: "passportData" }); + }, + + clearSecretFromStorage: async () => { + await Keychain.resetGenericPassword({ service: "secret" }); + }, update: (patch) => { set({ diff --git a/app/src/utils/nfcScanner.ts b/app/src/utils/nfcScanner.ts index 4edcc396d..34d16abef 100644 --- a/app/src/utils/nfcScanner.ts +++ b/app/src/utils/nfcScanner.ts @@ -40,32 +40,34 @@ export const scan = async () => { setStep(Steps.NFC_SCANNING); if (Platform.OS === 'android') { - scanAndroid(setStep, toast); + scanAndroid(); } else { - scanIOS(setStep, toast); + scanIOS(); } }; -const scanAndroid = async ( - setStep: (value: number) => void, - toast: any -) => { - const userState = useUserStore.getState() +const scanAndroid = async () => { + const { + passportNumber, + dateOfBirth, + dateOfExpiry + } = useUserStore.getState() + const {toast, setStep} = useNavigationStore.getState(); try { const response = await PassportReader.scan({ - documentNumber: userState.passportNumber, - dateOfBirth: userState.dateOfBirth, - dateOfExpiry: userState.dateOfExpiry + documentNumber: passportNumber, + dateOfBirth: dateOfBirth, + dateOfExpiry: dateOfExpiry }); console.log('scanned'); amplitude.track('NFC scan successful'); - handleResponseAndroid(response, setStep); + handleResponseAndroid(response); } catch (e: any) { console.log('error during scan:', e); setStep(Steps.MRZ_SCAN_COMPLETED); amplitude.track('NFC scan unsuccessful', { error: JSON.stringify(e) }); - toast.show('Error', { + toast?.show('Error', { message: e.message, customData: { type: "error", @@ -75,27 +77,29 @@ const scanAndroid = async ( } }; -const scanIOS = async ( - setStep: (value: number) => void, - toast: any - ) => { - const userState = useUserStore.getState() +const scanIOS = async () => { + const { + passportNumber, + dateOfBirth, + dateOfExpiry + } = useUserStore.getState() + const {toast, setStep} = useNavigationStore.getState(); try { const response = await NativeModules.PassportReader.scanPassport( - userState.passportNumber, - userState.dateOfBirth, - userState.dateOfExpiry + passportNumber, + dateOfBirth, + dateOfExpiry ); console.log('scanned'); - handleResponseIOS(response, setStep); + handleResponseIOS(response); amplitude.track('NFC scan successful'); } catch (e: any) { console.log('error during scan:', e); setStep(Steps.MRZ_SCAN_COMPLETED); amplitude.track(`NFC scan unsuccessful, error ${e.message}`); if (!e.message.includes("UserCanceled")) { - toast.show('Failed to read passport', { + toast?.show('Failed to read passport', { message: e.message, customData: { type: "error", @@ -107,7 +111,6 @@ const scanIOS = async ( const handleResponseIOS = async ( response: any, - setStep: (value: number) => void, ) => { const parsed = JSON.parse(response); @@ -164,15 +167,12 @@ const handleResponseIOS = async ( // console.log('passportData', JSON.stringify(passportData, null, 2)); - useUserStore.setState({ - passportData - }) - setStep(Steps.NFC_SCAN_COMPLETED); + useUserStore.getState().registerPassportData(passportData) + useNavigationStore.getState().setStep(Steps.NFC_SCAN_COMPLETED); }; const handleResponseAndroid = async ( response: any, - setStep: (value: number) => void, ) => { const { mrz, @@ -221,9 +221,6 @@ const handleResponseAndroid = async ( console.log("unicodeVersion", unicodeVersion) console.log("encapContent", encapContent) - useUserStore.setState({ - passportData - }) - - setStep(Steps.NFC_SCAN_COMPLETED); + useUserStore.getState().registerPassportData(passportData) + useNavigationStore.getState().setStep(Steps.NFC_SCAN_COMPLETED); }; \ No newline at end of file diff --git a/app/yarn.lock b/app/yarn.lock index b15c24689..bddaece54 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -1702,7 +1702,7 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@react-native-async-storage/async-storage@^1.17.11", "@react-native-async-storage/async-storage@^1.23.1": +"@react-native-async-storage/async-storage@^1.17.11": version "1.23.1" resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.23.1.tgz#cad3cd4fab7dacfe9838dce6ecb352f79150c883" integrity sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA== @@ -4817,6 +4817,11 @@ express@^4.18.2: utils-merge "1.0.1" vary "~1.1.2" +fast-base64-decode@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" + integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -7597,6 +7602,18 @@ react-native-fs@^2.20.0: base-64 "^0.1.0" utf8 "^3.0.0" +react-native-get-random-values@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz#1ca70d1271f4b08af92958803b89dccbda78728d" + integrity sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ== + dependencies: + fast-base64-decode "^1.0.0" + +react-native-keychain@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/react-native-keychain/-/react-native-keychain-8.2.0.tgz#aea82df37aacbb04f8b567a8e0e6d7292025610a" + integrity sha512-SkRtd9McIl1Ss2XSWNLorG+KMEbgeVqX+gV+t3u1EAAqT8q2/OpRmRbxpneT2vnb/dMhiU7g6K/pf3nxLUXRvA== + react-native-passport-reader@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/react-native-passport-reader/-/react-native-passport-reader-1.0.3.tgz#3242bbdb3c1ade4c050a8632cca6f11fe0edc648"