generating and storing secret, storing passport data securely

This commit is contained in:
0xturboblitz
2024-05-14 17:10:50 +09:00
parent e0fa32841f
commit 319ebade38
10 changed files with 253 additions and 63 deletions

View File

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

View File

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

View File

@@ -31,9 +31,11 @@
<key>NFCReaderUsageDescription</key>
<string>Need NFC to read Passport</string>
<key>NSAppTransportSecurity</key>
<dict/>
<string></string>
<key>NSFaceIDUsageDescription</key>
<string>Needed to secure the secret</string>
<key>NSCameraUsageDescription</key>
<string>Needed to scan your passport MRZ, you can however enter it manually.</string>
<string>Needed to scan the passport MRZ.</string>
<key>NSHumanReadableCopyright</key>
<string></string>
<key>NSLocationWhenInUseUsageDescription</key>

View File

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

View File

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

View File

@@ -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 = () => {
<VenetianMask color={textColor1} />
</Button>
</Fieldset>
<Fieldset gap="$4" mt="$1" horizontal>
<Label color={textColor1} width={200} justifyContent="flex-end" htmlFor="skip" >
Delete passport data
</Label>
<Button bg={componentBgColor} jc="center" borderColor={borderColor} borderWidth={1.2} size="$3.5" ml="$2" onPress={clearPassportDataFromStorage}>
<VenetianMask color={textColor1} />
</Button>
</Fieldset>
<Fieldset gap="$4" mt="$1" horizontal>
<Label color={textColor1} width={200} justifyContent="flex-end" htmlFor="skip" >
Delete secret (caution)
</Label>
<Button bg={componentBgColor} jc="center" borderColor={borderColor} borderWidth={1.2} size="$3.5" ml="$2" onPress={clearSecretFromStorage}>
<VenetianMask color={textColor1} />
</Button>
</Fieldset>
<YStack flex={1} />
<YStack mb="$0">

View File

@@ -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 ? (
<XStack ai="center" gap="$1">
<Spinner />
<Text color={textColor1} fow="bold">
Registering identity...
</Text>
</XStack>
) : isZkeyDownloading[selectedApp.circuit] ? (
<XStack ai="center" gap="$1">
<Spinner />
<Text color={textColor1} fow="bold">

View File

@@ -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<UserState>((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({

View File

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

View File

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