mirror of
https://github.com/selfxyz/self.git
synced 2026-04-05 03:00:53 -04:00
fix: refactor secret and passport-data management (#459)
* fix: refactor secret and passport-data management * clean things up * hadle biometric availablity state * added unsafe_secret_privateKey & unsafe_clearSecrets in usePassport for devSettingsScreen * remove setting of secret in loadingScreen as it's handled in usePassport internally
This commit is contained in:
@@ -3,6 +3,7 @@ import React, { useCallback, useState } from 'react';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import { Button, Text, XStack, YStack } from 'tamagui';
|
||||
|
||||
import { usePassport } from '../stores/passportDataProvider';
|
||||
import {
|
||||
black,
|
||||
slate50,
|
||||
@@ -15,8 +16,7 @@ import {
|
||||
import { confirmTap } from '../utils/haptic';
|
||||
|
||||
interface MnemonicProps {
|
||||
words?: string[];
|
||||
onRevealWords?: () => Promise<void>;
|
||||
onRevealWords?: () => void;
|
||||
}
|
||||
interface WordPill {
|
||||
index: number;
|
||||
@@ -46,20 +46,20 @@ const WordPill = ({ index, word }: WordPill) => {
|
||||
const REDACTED = new Array(24)
|
||||
.fill('')
|
||||
.map(_ => '*'.repeat(Math.max(4, Math.floor(Math.random() * 10))));
|
||||
const Mnemonic = ({ words = REDACTED, onRevealWords }: MnemonicProps) => {
|
||||
const Mnemonic = ({ onRevealWords }: MnemonicProps) => {
|
||||
const { secret } = usePassport();
|
||||
const [revealWords, setRevealWords] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copyToClipboardOrReveal = useCallback(async () => {
|
||||
confirmTap();
|
||||
if (!revealWords) {
|
||||
// TODO: container jumps when words are revealed on android
|
||||
await onRevealWords?.();
|
||||
onRevealWords?.();
|
||||
return setRevealWords(previous => !previous);
|
||||
}
|
||||
Clipboard.setString(words.join(' '));
|
||||
Clipboard.setString(secret?.phrase || '');
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2500);
|
||||
}, [words, revealWords]);
|
||||
}, [secret, revealWords, onRevealWords]);
|
||||
|
||||
return (
|
||||
<YStack position="relative" alignItems="stretch" gap={0}>
|
||||
@@ -75,9 +75,11 @@ const Mnemonic = ({ words = REDACTED, onRevealWords }: MnemonicProps) => {
|
||||
paddingVertical={28}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
{(revealWords ? words : REDACTED).map((word, i) => (
|
||||
<WordPill key={i} word={word} index={i} />
|
||||
))}
|
||||
{(revealWords && secret ? secret.phrase.split(' ') : REDACTED).map(
|
||||
(word, i) => (
|
||||
<WordPill key={i + word} word={word} index={i} />
|
||||
),
|
||||
)}
|
||||
</XStack>
|
||||
<XStack
|
||||
borderTopColor={slate200}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
import { useAuth } from '../stores/authProvider';
|
||||
|
||||
export default function useMnemonic() {
|
||||
const { getOrCreateMnemonic } = useAuth();
|
||||
const [mnemonic, setMnemonic] = useState<string[]>();
|
||||
|
||||
const loadMnemonic = useCallback(async () => {
|
||||
const storedMnemonic = await getOrCreateMnemonic();
|
||||
if (!storedMnemonic) {
|
||||
return;
|
||||
}
|
||||
const { entropy } = storedMnemonic.data;
|
||||
setMnemonic(ethers.Mnemonic.fromEntropy(entropy).phrase.split(' '));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
loadMnemonic,
|
||||
mnemonic,
|
||||
};
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import Keyboard from '../../images/icons/keyboard.svg';
|
||||
import RestoreAccountSvg from '../../images/icons/restore_account.svg';
|
||||
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
|
||||
import { useAuth } from '../../stores/authProvider';
|
||||
import { loadPassportDataAndSecret } from '../../stores/passportDataProvider';
|
||||
import { usePassport } from '../../stores/passportDataProvider';
|
||||
import { useSettingStore } from '../../stores/settingStore';
|
||||
import { STORAGE_NAME, useBackupMnemonic } from '../../utils/cloudBackup';
|
||||
import { black, slate500, slate600, white } from '../../utils/colors';
|
||||
@@ -24,13 +24,12 @@ interface AccountRecoveryChoiceScreenProps {}
|
||||
const AccountRecoveryChoiceScreen: React.FC<
|
||||
AccountRecoveryChoiceScreenProps
|
||||
> = ({}) => {
|
||||
const { restoreAccountFromMnemonic } = useAuth();
|
||||
const { passportData, restorefromSecret, status } = usePassport();
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
const { cloudBackupEnabled, toggleCloudBackupEnabled, biometricsAvailable } =
|
||||
useSettingStore();
|
||||
const { cloudBackupEnabled, toggleCloudBackupEnabled } = useSettingStore();
|
||||
const { biometricAvailablity } = useAuth();
|
||||
const { download } = useBackupMnemonic();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const onRestoreFromCloudNext = useHapticNavigation('AccountVerifiedSuccess');
|
||||
const onEnterRecoveryPress = useHapticNavigation('RecoverWithPhrase');
|
||||
|
||||
@@ -38,44 +37,57 @@ const AccountRecoveryChoiceScreen: React.FC<
|
||||
setRestoring(true);
|
||||
try {
|
||||
const mnemonic = await download();
|
||||
const result = await restoreAccountFromMnemonic(mnemonic.phrase);
|
||||
try {
|
||||
if (status !== 'success') {
|
||||
return;
|
||||
}
|
||||
const secret = await restorefromSecret(mnemonic.phrase);
|
||||
if (!secret || !passportData) {
|
||||
console.warn('Secret or passport data is missing');
|
||||
navigation.navigate('Launch');
|
||||
setRestoring(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
console.warn('Failed to restore account');
|
||||
navigation.navigate('Launch');
|
||||
setRestoring(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const passportDataAndSecret =
|
||||
(await loadPassportDataAndSecret()) as string;
|
||||
const { passportData, secret } = JSON.parse(passportDataAndSecret);
|
||||
const isRegistered = await isUserRegistered(passportData, secret);
|
||||
console.log('User is registered:', isRegistered);
|
||||
if (!isRegistered) {
|
||||
console.log(
|
||||
'Secret provided did not match a registered passport. Please try again.',
|
||||
const isRegistered = await isUserRegistered(
|
||||
passportData,
|
||||
secret.password,
|
||||
);
|
||||
navigation.navigate('Launch');
|
||||
setRestoring(false);
|
||||
return;
|
||||
}
|
||||
console.log('User is registered:', isRegistered);
|
||||
if (!isRegistered) {
|
||||
console.log(
|
||||
'Secret provided did not match a registered passport. Please try again.',
|
||||
);
|
||||
navigation.navigate('Launch');
|
||||
setRestoring(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cloudBackupEnabled) {
|
||||
toggleCloudBackupEnabled();
|
||||
if (!cloudBackupEnabled) {
|
||||
toggleCloudBackupEnabled();
|
||||
}
|
||||
onRestoreFromCloudNext();
|
||||
setRestoring(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setRestoring(false);
|
||||
throw new Error('Something wrong happened during cloud recovery');
|
||||
}
|
||||
onRestoreFromCloudNext();
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore account');
|
||||
navigation.navigate('Launch');
|
||||
setRestoring(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setRestoring(false);
|
||||
throw new Error('Something wrong happened during cloud recovery');
|
||||
return;
|
||||
}
|
||||
}, [
|
||||
cloudBackupEnabled,
|
||||
download,
|
||||
restoreAccountFromMnemonic,
|
||||
restorefromSecret,
|
||||
onRestoreFromCloudNext,
|
||||
navigation.navigate,
|
||||
passportData,
|
||||
toggleCloudBackupEnabled,
|
||||
status,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -91,7 +103,7 @@ const AccountRecoveryChoiceScreen: React.FC<
|
||||
<Description>
|
||||
By continuing, you certify that this passport belongs to you and is
|
||||
not stolen or forged.{' '}
|
||||
{biometricsAvailable && (
|
||||
{biometricAvailablity === 'available' && (
|
||||
<>
|
||||
Your device doesn't support biometrics or is disabled for apps
|
||||
and is required for cloud storage.
|
||||
@@ -102,7 +114,7 @@ const AccountRecoveryChoiceScreen: React.FC<
|
||||
<YStack gap="$2.5" width="100%" pt="$6">
|
||||
<PrimaryButton
|
||||
onPress={onRestoreFromCloudPress}
|
||||
disabled={restoring || !biometricsAvailable}
|
||||
disabled={restoring || biometricAvailablity !== 'available'}
|
||||
>
|
||||
{restoring ? 'Restoring' : 'Restore'} from {STORAGE_NAME}
|
||||
{restoring ? '…' : ''}
|
||||
|
||||
@@ -9,8 +9,7 @@ import { Text, TextArea, View, XStack, YStack } from 'tamagui';
|
||||
import { SecondaryButton } from '../../components/buttons/SecondaryButton';
|
||||
import Description from '../../components/typography/Description';
|
||||
import Paste from '../../images/icons/paste.svg';
|
||||
import { useAuth } from '../../stores/authProvider';
|
||||
import { loadPassportDataAndSecret } from '../../stores/passportDataProvider';
|
||||
import { usePassport } from '../../stores/passportDataProvider';
|
||||
import {
|
||||
black,
|
||||
slate300,
|
||||
@@ -27,7 +26,7 @@ const RecoverWithPhraseScreen: React.FC<
|
||||
RecoverWithPhraseScreenProps
|
||||
> = ({}) => {
|
||||
const navigation = useNavigation();
|
||||
const { restoreAccountFromMnemonic } = useAuth();
|
||||
const { restorefromSecret, passportData, status } = usePassport();
|
||||
const [mnemonic, setMnemonic] = useState<string>();
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
const onPaste = useCallback(async () => {
|
||||
@@ -39,6 +38,9 @@ const RecoverWithPhraseScreen: React.FC<
|
||||
}, []);
|
||||
|
||||
const restoreAccount = useCallback(async () => {
|
||||
if (status !== 'success') {
|
||||
return;
|
||||
}
|
||||
setRestoring(true);
|
||||
const slimMnemonic = mnemonic?.trim();
|
||||
if (!slimMnemonic || !ethers.Mnemonic.isValidMnemonic(slimMnemonic)) {
|
||||
@@ -46,31 +48,39 @@ const RecoverWithPhraseScreen: React.FC<
|
||||
setRestoring(false);
|
||||
return;
|
||||
}
|
||||
const result = await restoreAccountFromMnemonic(slimMnemonic);
|
||||
|
||||
if (!result) {
|
||||
try {
|
||||
const secret = await restorefromSecret(slimMnemonic);
|
||||
if (!passportData || !secret) {
|
||||
console.warn('Secret or passport data is missing');
|
||||
navigation.navigate('Launch');
|
||||
setRestoring(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isRegistered = await isUserRegistered(
|
||||
passportData,
|
||||
secret.password,
|
||||
);
|
||||
console.log('User is registered:', isRegistered);
|
||||
if (!isRegistered) {
|
||||
console.log(
|
||||
'Secret provided did not match a registered passport. Please try again.',
|
||||
);
|
||||
navigation.navigate('Launch');
|
||||
setRestoring(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setRestoring(false);
|
||||
navigation.navigate('AccountVerifiedSuccess');
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore account');
|
||||
navigation.navigate('Launch');
|
||||
setRestoring(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const passportDataAndSecret = (await loadPassportDataAndSecret()) as string;
|
||||
const { passportData, secret } = JSON.parse(passportDataAndSecret);
|
||||
const isRegistered = await isUserRegistered(passportData, secret);
|
||||
console.log('User is registered:', isRegistered);
|
||||
if (!isRegistered) {
|
||||
console.log(
|
||||
'Secret provided did not match a registered passport. Please try again.',
|
||||
);
|
||||
navigation.navigate('Launch');
|
||||
setRestoring(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setRestoring(false);
|
||||
navigation.navigate('AccountVerifiedSuccess');
|
||||
}, [mnemonic, restoreAccountFromMnemonic]);
|
||||
}, [mnemonic, restorefromSecret, navigation, passportData, status]);
|
||||
|
||||
return (
|
||||
<YStack alignItems="center" gap="$6" pb="$2.5" style={styles.layout}>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Caption } from '../../components/typography/Caption';
|
||||
import Description from '../../components/typography/Description';
|
||||
import { Title } from '../../components/typography/Title';
|
||||
import useHapticNavigation from '../../hooks/useHapticNavigation';
|
||||
import useMnemonic from '../../hooks/useMnemonic';
|
||||
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
|
||||
import { STORAGE_NAME } from '../../utils/cloudBackup';
|
||||
import { black, slate400, white } from '../../utils/colors';
|
||||
@@ -18,12 +17,6 @@ const SaveRecoveryPhraseScreen: React.FC<
|
||||
SaveRecoveryPhraseScreenProps
|
||||
> = ({}) => {
|
||||
const [userHasSeenMnemonic, setUserHasSeenMnemonic] = useState(false);
|
||||
const { mnemonic, loadMnemonic } = useMnemonic();
|
||||
|
||||
const onRevealWords = useCallback(async () => {
|
||||
await loadMnemonic();
|
||||
setUserHasSeenMnemonic(true);
|
||||
}, []);
|
||||
|
||||
const onCloudBackupPress = useHapticNavigation('CloudBackupSettings', {
|
||||
params: { nextScreen: 'SaveRecoveryPhrase' },
|
||||
@@ -31,6 +24,11 @@ const SaveRecoveryPhraseScreen: React.FC<
|
||||
const onSkipPress = useHapticNavigation('AccountVerifiedSuccess', {
|
||||
action: 'confirm',
|
||||
});
|
||||
const onRevealWords = useCallback(() => {
|
||||
() => {
|
||||
setUserHasSeenMnemonic(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
@@ -53,7 +51,7 @@ const SaveRecoveryPhraseScreen: React.FC<
|
||||
gap={10}
|
||||
backgroundColor={white}
|
||||
>
|
||||
<Mnemonic words={mnemonic} onRevealWords={onRevealWords} />
|
||||
<Mnemonic onRevealWords={onRevealWords} />
|
||||
<Caption color={slate400}>
|
||||
You can reveal your recovery phrase in settings.
|
||||
</Caption>
|
||||
|
||||
@@ -27,13 +27,14 @@ import { PrimaryButton } from '../components/buttons/PrimaryButton';
|
||||
import { SecondaryButton } from '../components/buttons/SecondaryButton';
|
||||
import { BodyText } from '../components/typography/BodyText';
|
||||
import { Title } from '../components/typography/Title';
|
||||
import { storePassportData } from '../stores/passportDataProvider';
|
||||
import { usePassport } from '../stores/passportDataProvider';
|
||||
import { borderColor, separatorColor, textBlack, white } from '../utils/colors';
|
||||
import { buttonTap, selectionChange } from '../utils/haptic';
|
||||
|
||||
interface MockDataScreenProps {}
|
||||
|
||||
const MockDataScreen: React.FC<MockDataScreenProps> = ({}) => {
|
||||
const MockDataScreen: React.FC<MockDataScreenProps> = () => {
|
||||
const { setPassportData } = usePassport(false);
|
||||
const navigation = useNavigation();
|
||||
const [age, setAge] = useState(24);
|
||||
const [expiryYears, setExpiryYears] = useState(5);
|
||||
@@ -194,7 +195,7 @@ const MockDataScreen: React.FC<MockDataScreenProps> = ({}) => {
|
||||
);
|
||||
}
|
||||
mockPassportData = initPassportDataParsing(mockPassportData);
|
||||
await storePassportData(mockPassportData);
|
||||
await setPassportData(mockPassportData);
|
||||
resolve(null);
|
||||
}, 0),
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({}) => {
|
||||
};
|
||||
const [animationSource, setAnimationSource] = useState<any>(miscAnimation);
|
||||
const { registrationStatus, resetProof } = useProofInfo();
|
||||
const { getPassportDataAndSecret, clearPassportData } = usePassport();
|
||||
const { passportData, clearPassportData, secret, status } = usePassport();
|
||||
|
||||
useEffect(() => {
|
||||
// TODO this makes sense if reset proof was only about passport registration
|
||||
@@ -63,17 +63,20 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({}) => {
|
||||
}, [registrationStatus]);
|
||||
|
||||
const processPayloadCalled = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!processPayloadCalled.current) {
|
||||
processPayloadCalled.current = true;
|
||||
const processPayload = async () => {
|
||||
try {
|
||||
const passportDataAndSecret = await getPassportDataAndSecret();
|
||||
if (!passportDataAndSecret) {
|
||||
if (status !== 'success') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passportData || !secret) {
|
||||
console.warn('no passportData or secret');
|
||||
navigation.navigate('Launch');
|
||||
return;
|
||||
}
|
||||
const { passportData, secret } = passportDataAndSecret.data;
|
||||
const isSupported = await checkPassportSupported(passportData);
|
||||
if (isSupported.status !== 'passport_supported') {
|
||||
trackEvent('Passport not supported', {
|
||||
@@ -85,7 +88,10 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({}) => {
|
||||
clearPassportData();
|
||||
return;
|
||||
}
|
||||
const isRegistered = await isUserRegistered(passportData, secret);
|
||||
const isRegistered = await isUserRegistered(
|
||||
passportData,
|
||||
secret.password,
|
||||
);
|
||||
console.log('User is registered:', isRegistered);
|
||||
if (isRegistered) {
|
||||
console.log(
|
||||
@@ -103,7 +109,7 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({}) => {
|
||||
navigation.navigate('AccountRecoveryChoice');
|
||||
return;
|
||||
}
|
||||
registerPassport(passportData, secret);
|
||||
registerPassport(passportData, secret.password);
|
||||
} catch (error) {
|
||||
console.error('Error processing payload:', error);
|
||||
setTimeout(() => resetProof(), 1000);
|
||||
@@ -111,7 +117,15 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({}) => {
|
||||
};
|
||||
processPayload();
|
||||
}
|
||||
}, []);
|
||||
}, [
|
||||
clearPassportData,
|
||||
goToUnsupportedScreen,
|
||||
passportData,
|
||||
secret,
|
||||
navigation.navigate,
|
||||
resetProof,
|
||||
status,
|
||||
]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Title } from '../../components/typography/Title';
|
||||
import useHapticNavigation from '../../hooks/useHapticNavigation';
|
||||
import NFC_IMAGE from '../../images/nfc.png';
|
||||
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
|
||||
import { storePassportData } from '../../stores/passportDataProvider';
|
||||
import { usePassport } from '../../stores/passportDataProvider';
|
||||
import useUserStore from '../../stores/userStore';
|
||||
import analytics from '../../utils/analytics';
|
||||
import { black, slate100, white } from '../../utils/colors';
|
||||
@@ -42,6 +42,7 @@ const emitter =
|
||||
: null;
|
||||
|
||||
const PassportNFCScanScreen: React.FC<PassportNFCScanScreenProps> = ({}) => {
|
||||
const { setPassportData } = usePassport();
|
||||
const navigation = useNavigation();
|
||||
const { passportNumber, dateOfBirth, dateOfExpiry } = useUserStore();
|
||||
const [dialogMessage, setDialogMessage] = useState('');
|
||||
@@ -142,7 +143,7 @@ const PassportNFCScanScreen: React.FC<PassportNFCScanScreenProps> = ({}) => {
|
||||
passportMetadata.cscaSignatureAlgorithmBits,
|
||||
dsc: passportMetadata.dsc,
|
||||
});
|
||||
await storePassportData(parsedPassportData);
|
||||
await setPassportData(parsedPassportData);
|
||||
// Feels better somehow
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
navigation.navigate('ConfirmBelongingScreen');
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
|
||||
const ProveScreen: React.FC = () => {
|
||||
const { navigate } = useNavigation();
|
||||
const { getPassportDataAndSecret } = usePassport();
|
||||
const { passportData, secret, status: passportStatus } = usePassport();
|
||||
const { selectedApp, resetProof, cleanSelfApp } = useProofInfo();
|
||||
const { handleProofVerified } = useApp();
|
||||
const selectedAppRef = useRef(selectedApp);
|
||||
@@ -107,9 +107,10 @@ const ProveScreen: React.FC = () => {
|
||||
|
||||
const onVerify = useCallback(
|
||||
async function () {
|
||||
if (isProcessing.current) {
|
||||
if (passportStatus !== 'success' || isProcessing.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessing.current = true;
|
||||
|
||||
resetProof();
|
||||
@@ -119,25 +120,20 @@ const ProveScreen: React.FC = () => {
|
||||
try {
|
||||
let timeToNavigateToStatusScreen: NodeJS.Timeout;
|
||||
|
||||
const passportDataAndSecret = await getPassportDataAndSecret().catch(
|
||||
(e: Error) => {
|
||||
console.error('Error getting passport data', e);
|
||||
globalSetDisclosureStatus?.(ProofStatusEnum.ERROR);
|
||||
},
|
||||
);
|
||||
|
||||
timeToNavigateToStatusScreen = setTimeout(() => {
|
||||
navigate('ProofRequestStatusScreen');
|
||||
}, 1000);
|
||||
|
||||
if (!passportDataAndSecret) {
|
||||
if (!passportData || !secret) {
|
||||
console.log('No passport data or secret');
|
||||
globalSetDisclosureStatus?.(ProofStatusEnum.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
const { passportData, secret } = passportDataAndSecret.data;
|
||||
const isRegistered = await isUserRegistered(passportData, secret);
|
||||
const isRegistered = await isUserRegistered(
|
||||
passportData,
|
||||
secret.password,
|
||||
);
|
||||
console.log('isRegistered', isRegistered);
|
||||
|
||||
if (!isRegistered) {
|
||||
@@ -152,7 +148,7 @@ const ProveScreen: React.FC = () => {
|
||||
|
||||
console.log('currentApp', currentApp);
|
||||
const status = await sendVcAndDisclosePayload(
|
||||
secret,
|
||||
secret.password,
|
||||
passportData,
|
||||
currentApp,
|
||||
);
|
||||
@@ -167,7 +163,14 @@ const ProveScreen: React.FC = () => {
|
||||
isProcessing.current = false;
|
||||
}
|
||||
},
|
||||
[navigate, getPassportDataAndSecret, handleProofVerified, resetProof],
|
||||
[
|
||||
navigate,
|
||||
handleProofVerified,
|
||||
resetProof,
|
||||
cleanSelfApp,
|
||||
passportData,
|
||||
secret,
|
||||
],
|
||||
);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useModal } from '../../hooks/useModal';
|
||||
import Cloud from '../../images/icons/logo_cloud_backup.svg';
|
||||
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
|
||||
import { useAuth } from '../../stores/authProvider';
|
||||
import { usePassport } from '../../stores/passportDataProvider';
|
||||
import { useSettingStore } from '../../stores/settingStore';
|
||||
import { STORAGE_NAME, useBackupMnemonic } from '../../utils/cloudBackup';
|
||||
import { black, white } from '../../utils/colors';
|
||||
@@ -32,9 +33,11 @@ interface CloudBackupScreenProps
|
||||
const CloudBackupScreen: React.FC<CloudBackupScreenProps> = ({
|
||||
route: { params },
|
||||
}) => {
|
||||
const { getOrCreateMnemonic, loginWithBiometrics } = useAuth();
|
||||
const { cloudBackupEnabled, toggleCloudBackupEnabled, biometricsAvailable } =
|
||||
useSettingStore();
|
||||
const { loginWithBiometrics } = useAuth();
|
||||
const { secret, status } = usePassport();
|
||||
const { cloudBackupEnabled, toggleCloudBackupEnabled } = useSettingStore();
|
||||
const { biometricAvailablity } = useAuth();
|
||||
|
||||
const { upload, disableBackup } = useBackupMnemonic();
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
@@ -64,26 +67,20 @@ const CloudBackupScreen: React.FC<CloudBackupScreenProps> = ({
|
||||
|
||||
const enableCloudBackups = useCallback(async () => {
|
||||
buttonTap();
|
||||
if (cloudBackupEnabled) {
|
||||
if (cloudBackupEnabled || status !== 'success') {
|
||||
return;
|
||||
}
|
||||
|
||||
setPending(true);
|
||||
|
||||
const storedMnemonic = await getOrCreateMnemonic();
|
||||
if (!storedMnemonic) {
|
||||
if (!secret) {
|
||||
setPending(false);
|
||||
return;
|
||||
}
|
||||
await upload(storedMnemonic.data);
|
||||
await upload(secret);
|
||||
toggleCloudBackupEnabled();
|
||||
setPending(false);
|
||||
}, [
|
||||
cloudBackupEnabled,
|
||||
getOrCreateMnemonic,
|
||||
upload,
|
||||
toggleCloudBackupEnabled,
|
||||
]);
|
||||
}, [cloudBackupEnabled, upload, toggleCloudBackupEnabled, secret]);
|
||||
|
||||
const disableCloudBackups = useCallback(() => {
|
||||
confirmTap();
|
||||
@@ -112,7 +109,7 @@ const CloudBackupScreen: React.FC<CloudBackupScreenProps> = ({
|
||||
: `Your account will be end-to-end encrypted backed up to ${STORAGE_NAME} so you can easily restore it if you ever get a new phone.`}
|
||||
</Description>
|
||||
<Caption>
|
||||
{biometricsAvailable ? (
|
||||
{biometricAvailablity === 'available' ? (
|
||||
<>
|
||||
Learn more about <BackupDocumentationLink />
|
||||
</>
|
||||
@@ -128,7 +125,7 @@ const CloudBackupScreen: React.FC<CloudBackupScreenProps> = ({
|
||||
{cloudBackupEnabled ? (
|
||||
<SecondaryButton
|
||||
onPress={disableCloudBackups}
|
||||
disabled={pending || !biometricsAvailable}
|
||||
disabled={pending || biometricAvailablity !== 'available'}
|
||||
>
|
||||
{pending ? 'Disabling' : 'Disable'} {STORAGE_NAME} backups
|
||||
{pending ? '…' : ''}
|
||||
@@ -136,7 +133,7 @@ const CloudBackupScreen: React.FC<CloudBackupScreenProps> = ({
|
||||
) : (
|
||||
<PrimaryButton
|
||||
onPress={enableCloudBackups}
|
||||
disabled={pending || !biometricsAvailable}
|
||||
disabled={pending || biometricAvailablity !== 'available'}
|
||||
>
|
||||
{pending ? 'Enabling' : 'Enable'} {STORAGE_NAME} backups
|
||||
{pending ? '…' : ''}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { PropsWithChildren, useEffect, useState } from 'react';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { Platform, TextInput } from 'react-native';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
@@ -22,14 +22,7 @@ import {
|
||||
|
||||
import { genMockPassportData } from '../../../../common/src/utils/passports/genMockPassportData';
|
||||
import { RootStackParamList } from '../../Navigation';
|
||||
import {
|
||||
unsafe_clearSecrets,
|
||||
unsafe_getPrivateKey,
|
||||
} from '../../stores/authProvider';
|
||||
import {
|
||||
storePassportData,
|
||||
usePassport,
|
||||
} from '../../stores/passportDataProvider';
|
||||
import { usePassport } from '../../stores/passportDataProvider';
|
||||
import { borderColor, textBlack } from '../../utils/colors';
|
||||
|
||||
interface DevSettingsScreenProps {}
|
||||
@@ -132,18 +125,26 @@ const ScreenSelector = ({}) => {
|
||||
};
|
||||
|
||||
const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
const { clearPassportData } = usePassport();
|
||||
const [privateKey, setPrivateKey] = useState('Loading private key…');
|
||||
const {
|
||||
clearPassportData,
|
||||
setPassportData,
|
||||
status,
|
||||
unsafe_clearSecrets,
|
||||
unsafe_secret_privateKey,
|
||||
} = usePassport();
|
||||
|
||||
const nav = useNavigation();
|
||||
|
||||
async function handleRestart() {
|
||||
if (status !== 'success') {
|
||||
return;
|
||||
}
|
||||
await clearPassportData();
|
||||
nav.navigate('Launch');
|
||||
}
|
||||
|
||||
async function deleteEverything() {
|
||||
await unsafe_clearSecrets();
|
||||
await clearPassportData();
|
||||
await handleRestart();
|
||||
}
|
||||
|
||||
@@ -156,13 +157,9 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
'000101',
|
||||
'300101',
|
||||
);
|
||||
storePassportData(passportData);
|
||||
setPassportData(passportData);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
unsafe_getPrivateKey().then(setPrivateKey);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<YStack gap="$3" mt="$2" ai="center">
|
||||
<Fieldset px="$4" horizontal width="100%" justifyContent="space-between">
|
||||
@@ -266,7 +263,7 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
userSelect="all"
|
||||
style={{ fontFamily: 'monospace', fontWeight: 'bold' }}
|
||||
>
|
||||
{privateKey}
|
||||
{unsafe_secret_privateKey ?? ''}
|
||||
</SelectableText>
|
||||
</Fieldset>
|
||||
</YStack>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { ScrollView, Separator, XStack, YStack } from 'tamagui';
|
||||
|
||||
import { PassportMetadata } from '../../../../common/src/utils/passports/passport_parsing/parsePassportData';
|
||||
@@ -54,27 +53,7 @@ const InfoRow: React.FC<{
|
||||
interface PassportDataInfoScreenProps {}
|
||||
|
||||
const PassportDataInfoScreen: React.FC<PassportDataInfoScreenProps> = ({}) => {
|
||||
const { getData } = usePassport();
|
||||
const [metadata, setMetadata] = useState<PassportMetadata | null>(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
if (metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await getData();
|
||||
|
||||
if (!result || !result.data) {
|
||||
// maybe handle error instead
|
||||
return;
|
||||
}
|
||||
|
||||
setMetadata(result.data.passportMetadata!);
|
||||
}, [metadata, getData]);
|
||||
|
||||
useFocusEffect(() => {
|
||||
loadData();
|
||||
});
|
||||
const { passportData } = usePassport();
|
||||
|
||||
return (
|
||||
<ScrollView px="$4" backgroundColor={white}>
|
||||
@@ -84,15 +63,15 @@ const PassportDataInfoScreen: React.FC<PassportDataInfoScreenProps> = ({}) => {
|
||||
key={key}
|
||||
label={label}
|
||||
value={
|
||||
!metadata
|
||||
!passportData?.passportMetadata
|
||||
? ''
|
||||
: key === 'cscaFound'
|
||||
? metadata?.cscaFound === true
|
||||
? passportData.passportMetadata.cscaFound === true
|
||||
? 'Yes'
|
||||
: 'No'
|
||||
: (metadata?.[key as keyof PassportMetadata] as
|
||||
| string
|
||||
| number) || 'None'
|
||||
: (passportData.passportMetadata?.[
|
||||
key as keyof PassportMetadata
|
||||
] as string | number) || 'None'
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import Mnemonic from '../../components/Mnemonic';
|
||||
import Description from '../../components/typography/Description';
|
||||
import useMnemonic from '../../hooks/useMnemonic';
|
||||
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
|
||||
|
||||
interface ShowRecoveryPhraseScreenProps {}
|
||||
@@ -10,12 +9,6 @@ interface ShowRecoveryPhraseScreenProps {}
|
||||
const ShowRecoveryPhraseScreen: React.FC<
|
||||
ShowRecoveryPhraseScreenProps
|
||||
> = ({}) => {
|
||||
const { mnemonic, loadMnemonic } = useMnemonic();
|
||||
|
||||
const onRevealWords = useCallback(async () => {
|
||||
await loadMnemonic();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor="white">
|
||||
<ExpandableBottomLayout.BottomSection
|
||||
@@ -23,7 +16,7 @@ const ShowRecoveryPhraseScreen: React.FC<
|
||||
justifyContent="center"
|
||||
gap={20}
|
||||
>
|
||||
<Mnemonic words={mnemonic} onRevealWords={onRevealWords} />
|
||||
<Mnemonic />
|
||||
<Description>
|
||||
This phrase is the only way to recover your account. Keep it secret,
|
||||
keep it safe.
|
||||
|
||||
@@ -1,46 +1,35 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import LottieView from 'lottie-react-native';
|
||||
|
||||
import splashAnimation from '../assets/animations/splash.json';
|
||||
import { useAuth } from '../stores/authProvider';
|
||||
import { loadPassportDataAndSecret } from '../stores/passportDataProvider';
|
||||
import { useSettingStore } from '../stores/settingStore';
|
||||
import { usePassport } from '../stores/passportDataProvider';
|
||||
import { black } from '../utils/colors';
|
||||
import { impactLight } from '../utils/haptic';
|
||||
import { isUserRegistered } from '../utils/proving/payload';
|
||||
|
||||
const SplashScreen: React.FC = ({}) => {
|
||||
const navigation = useNavigation();
|
||||
const { createSigningKeyPair } = useAuth();
|
||||
const { setBiometricsAvailable } = useSettingStore();
|
||||
|
||||
useEffect(() => {
|
||||
createSigningKeyPair()
|
||||
.then(setBiometricsAvailable)
|
||||
.catch(err => {
|
||||
console.warn(
|
||||
'Something ELSE and totally unexpected went wrong during keypair creation',
|
||||
err,
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
const { passportData, secret, status } = usePassport(false);
|
||||
|
||||
const handleAnimationFinish = useCallback(() => {
|
||||
setTimeout(async () => {
|
||||
impactLight();
|
||||
const passportDataAndSecret = await loadPassportDataAndSecret();
|
||||
if (status !== 'success') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passportDataAndSecret) {
|
||||
if (!passportData || !secret) {
|
||||
navigation.navigate('Launch');
|
||||
return;
|
||||
}
|
||||
|
||||
const { passportData, secret } = JSON.parse(passportDataAndSecret);
|
||||
|
||||
const isRegistered = await isUserRegistered(passportData, secret);
|
||||
const isRegistered = await isUserRegistered(
|
||||
passportData,
|
||||
secret.password,
|
||||
);
|
||||
console.log('User is registered:', isRegistered);
|
||||
if (isRegistered) {
|
||||
console.log('Passport is registered already. Skipping to HomeScreen');
|
||||
@@ -56,7 +45,7 @@ const SplashScreen: React.FC = ({}) => {
|
||||
// Rest of the time, keep the LaunchScreen flow
|
||||
navigation.navigate('Launch');
|
||||
}, 1000);
|
||||
}, [navigation]);
|
||||
}, [navigation, passportData, secret, status]);
|
||||
|
||||
return (
|
||||
<LottieView
|
||||
|
||||
@@ -3,152 +3,46 @@ import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import ReactNativeBiometrics from 'react-native-biometrics';
|
||||
import Keychain from 'react-native-keychain';
|
||||
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
import { Mnemonic } from '../types/mnemonic';
|
||||
|
||||
const SERVICE_NAME = 'secret';
|
||||
|
||||
type SignedPayload<T> = { signature: string; data: T };
|
||||
const _getSecurely = async function <T>(
|
||||
fn: () => Promise<string | false>,
|
||||
formatter: (dataString: string) => T,
|
||||
): Promise<SignedPayload<T> | null> {
|
||||
console.log('Starting _getSecurely');
|
||||
|
||||
const keysExist = await biometrics.biometricKeysExist();
|
||||
console.log('Biometric keys exist:', keysExist.keysExist);
|
||||
|
||||
if (!keysExist.keysExist) {
|
||||
console.log('Creating new biometric keys');
|
||||
await biometrics.createKeys();
|
||||
}
|
||||
|
||||
const dataString = await fn();
|
||||
console.log('Got data string:', dataString ? 'exists' : 'not found');
|
||||
|
||||
if (dataString === false) {
|
||||
console.log('No data string available');
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchBiometricAvailablity(): Promise<boolean> {
|
||||
try {
|
||||
const simpleCheck = await biometrics.simplePrompt({
|
||||
promptMessage: 'Allow access to identity',
|
||||
});
|
||||
|
||||
if (!simpleCheck.success) {
|
||||
throw new Error('Authentication failed');
|
||||
}
|
||||
|
||||
return {
|
||||
signature: 'authenticated',
|
||||
data: formatter(dataString),
|
||||
};
|
||||
const { available } = await biometrics.isSensorAvailable();
|
||||
return available;
|
||||
} catch (error) {
|
||||
console.error('Error in _getSecurely:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
async function createSigningKeyPair(): Promise<boolean> {
|
||||
const { available } = await biometrics.isSensorAvailable();
|
||||
if (!available) {
|
||||
console.error('Error checking biometric availability:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((await biometrics.biometricKeysExist()).keysExist) {
|
||||
return true;
|
||||
}
|
||||
console.log('No enrolled public key. Creating a public key from biometrics');
|
||||
try {
|
||||
await biometrics.createKeys();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (available) {
|
||||
console.error(
|
||||
"User has biometrics but somehow it wasn't able to create keys",
|
||||
);
|
||||
return false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreFromMnemonic(mnemonic: string) {
|
||||
if (!mnemonic || !ethers.Mnemonic.isValidMnemonic(mnemonic)) {
|
||||
throw new Error('Invalid mnemonic');
|
||||
}
|
||||
|
||||
const restoredWallet = ethers.Wallet.fromPhrase(mnemonic);
|
||||
const data = JSON.stringify(restoredWallet.mnemonic);
|
||||
await Keychain.setGenericPassword('secret', data, {
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
async function loadOrCreateMnemonic() {
|
||||
const storedMnemonic = await Keychain.getGenericPassword({
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
if (storedMnemonic) {
|
||||
try {
|
||||
JSON.parse(storedMnemonic.password);
|
||||
console.log('Stored mnemonic parsed successfully');
|
||||
return storedMnemonic.password;
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'Error parsing stored mnemonic, old secret format was used',
|
||||
e,
|
||||
);
|
||||
console.log('Creating a new one');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('No secret found, creating one');
|
||||
const { mnemonic } = ethers.HDNodeWallet.fromMnemonic(
|
||||
ethers.Mnemonic.fromEntropy(ethers.randomBytes(32)),
|
||||
);
|
||||
const data = JSON.stringify(mnemonic);
|
||||
await Keychain.setGenericPassword('secret', data, {
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
const biometrics = new ReactNativeBiometrics({
|
||||
allowDeviceCredentials: true,
|
||||
});
|
||||
|
||||
interface AuthProviderProps extends PropsWithChildren {
|
||||
authenticationTimeoutinMs?: number;
|
||||
}
|
||||
type BiometricAvailablity =
|
||||
| 'unknown'
|
||||
| 'checking'
|
||||
| 'available'
|
||||
| 'unavailable';
|
||||
|
||||
interface IAuthContext {
|
||||
isAuthenticated: boolean;
|
||||
isAuthenticating: boolean;
|
||||
loginWithBiometrics: () => Promise<void>;
|
||||
_getSecurely: typeof _getSecurely;
|
||||
getOrCreateMnemonic: () => Promise<SignedPayload<Mnemonic> | null>;
|
||||
restoreAccountFromMnemonic: (
|
||||
mnemonic: string,
|
||||
) => Promise<SignedPayload<boolean> | null>;
|
||||
createSigningKeyPair: () => Promise<boolean>;
|
||||
biometricAvailablity: BiometricAvailablity;
|
||||
}
|
||||
export const AuthContext = createContext<IAuthContext>({
|
||||
isAuthenticated: false,
|
||||
isAuthenticating: false,
|
||||
loginWithBiometrics: () => Promise.resolve(),
|
||||
_getSecurely,
|
||||
getOrCreateMnemonic: () => Promise.resolve(null),
|
||||
restoreAccountFromMnemonic: () => Promise.resolve(null),
|
||||
createSigningKeyPair: () => Promise.resolve(false),
|
||||
biometricAvailablity: 'unknown',
|
||||
});
|
||||
|
||||
export const AuthProvider = ({
|
||||
@@ -158,68 +52,59 @@ export const AuthProvider = ({
|
||||
const [_, setAuthenticatedTimeout] =
|
||||
useState<ReturnType<typeof setTimeout>>();
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [isAuthenticatingPromise, setIsAuthenticatingPromise] =
|
||||
useState<Promise<{ success: boolean; error?: string }> | null>(null);
|
||||
const [biometricAvailablity, setBiometricAvailablity] =
|
||||
useState<BiometricAvailablity>('unknown');
|
||||
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchBiometricAvailablity();
|
||||
}, []);
|
||||
|
||||
const loginWithBiometrics = useCallback(async () => {
|
||||
if (isAuthenticatingPromise) {
|
||||
await isAuthenticatingPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
const promise = biometrics.simplePrompt({
|
||||
promptMessage: 'Confirm your identity to access the stored secret',
|
||||
});
|
||||
setIsAuthenticatingPromise(promise);
|
||||
const { success, error } = await promise;
|
||||
if (error) {
|
||||
setIsAuthenticatingPromise(null);
|
||||
// handle error
|
||||
throw error;
|
||||
}
|
||||
if (!success) {
|
||||
// user canceled
|
||||
throw new Error('Canceled by user');
|
||||
}
|
||||
|
||||
setIsAuthenticatingPromise(null);
|
||||
setIsAuthenticated(true);
|
||||
setAuthenticatedTimeout(previousTimeout => {
|
||||
if (previousTimeout) {
|
||||
clearTimeout(previousTimeout);
|
||||
setIsAuthenticating(true);
|
||||
try {
|
||||
const { success, error } = await biometrics.simplePrompt({
|
||||
promptMessage: 'Confirm your identity to access the stored secret',
|
||||
});
|
||||
if (error) {
|
||||
// handle error
|
||||
throw error;
|
||||
}
|
||||
setBiometricAvailablity('available');
|
||||
if (!success) {
|
||||
// user canceled
|
||||
throw new Error('Canceled by user');
|
||||
}
|
||||
return setTimeout(
|
||||
() => setIsAuthenticated(false),
|
||||
authenticationTimeoutinMs,
|
||||
);
|
||||
});
|
||||
}, [isAuthenticatingPromise]);
|
||||
|
||||
const getOrCreateMnemonic = useCallback(
|
||||
() => _getSecurely<Mnemonic>(loadOrCreateMnemonic, str => JSON.parse(str)),
|
||||
[],
|
||||
);
|
||||
|
||||
const restoreAccountFromMnemonic = useCallback(
|
||||
(mnemonic: string) =>
|
||||
_getSecurely<boolean>(
|
||||
() => restoreFromMnemonic(mnemonic),
|
||||
str => !!str,
|
||||
),
|
||||
[],
|
||||
);
|
||||
setIsAuthenticated(true);
|
||||
setAuthenticatedTimeout(previousTimeout => {
|
||||
if (previousTimeout) {
|
||||
clearTimeout(previousTimeout);
|
||||
}
|
||||
return setTimeout(
|
||||
() => setIsAuthenticated(false),
|
||||
authenticationTimeoutinMs,
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
setIsAuthenticating(false);
|
||||
}
|
||||
}, [authenticationTimeoutinMs]);
|
||||
|
||||
const state: IAuthContext = useMemo(
|
||||
() => ({
|
||||
isAuthenticated,
|
||||
isAuthenticating: !!isAuthenticatingPromise,
|
||||
isAuthenticating,
|
||||
loginWithBiometrics,
|
||||
getOrCreateMnemonic,
|
||||
restoreAccountFromMnemonic,
|
||||
createSigningKeyPair,
|
||||
_getSecurely,
|
||||
biometricAvailablity,
|
||||
}),
|
||||
[isAuthenticated, isAuthenticatingPromise, loginWithBiometrics],
|
||||
[
|
||||
isAuthenticated,
|
||||
isAuthenticating,
|
||||
loginWithBiometrics,
|
||||
biometricAvailablity,
|
||||
],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>;
|
||||
@@ -228,24 +113,3 @@ export const AuthProvider = ({
|
||||
export const useAuth = () => {
|
||||
return useContext(AuthContext);
|
||||
};
|
||||
|
||||
export async function hasSecretStored() {
|
||||
const seed = await Keychain.getGenericPassword({ service: SERVICE_NAME });
|
||||
return !!seed;
|
||||
}
|
||||
|
||||
/**
|
||||
* The only reason this is exported without being locked behind user biometrics is to allow `loadPassportDataAndSecret`
|
||||
* to access both the privatekey and the passport data with the user only authenticating once
|
||||
*/
|
||||
export async function unsafe_getPrivateKey() {
|
||||
const mnemonic = JSON.parse(await loadOrCreateMnemonic()) as Mnemonic;
|
||||
const wallet = ethers.HDNodeWallet.fromPhrase(mnemonic.phrase);
|
||||
return wallet.privateKey;
|
||||
}
|
||||
|
||||
export async function unsafe_clearSecrets() {
|
||||
if (__DEV__) {
|
||||
await Keychain.resetGenericPassword({ service: SERVICE_NAME });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,91 +3,184 @@ import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Keychain from 'react-native-keychain';
|
||||
|
||||
import { PassportData } from '../../../common/src/utils/types';
|
||||
import { unsafe_getPrivateKey } from '../stores/authProvider';
|
||||
import { useAuth } from './authProvider';
|
||||
import { type Mnemonic, ethers } from 'ethers';
|
||||
|
||||
// TODO: refactor this as it shouldnt be used directly IMHO
|
||||
export async function loadPassportData() {
|
||||
const passportDataCreds = await Keychain.getGenericPassword({
|
||||
import type { PassportData } from '../../../common/src/utils/types';
|
||||
import { useAuth } from '../stores/authProvider';
|
||||
|
||||
const password = 'passportData';
|
||||
const SERVICE_NAME = 'secret';
|
||||
|
||||
export async function hasSecretStored() {
|
||||
const seed = await Keychain.getGenericPassword({ service: SERVICE_NAME });
|
||||
return !!seed;
|
||||
}
|
||||
|
||||
async function storePassportDataInKeychain(passportData: PassportData) {
|
||||
await Keychain.setGenericPassword(password, JSON.stringify(passportData), {
|
||||
service: 'passportData',
|
||||
});
|
||||
return passportDataCreds === false ? false : passportDataCreds.password;
|
||||
}
|
||||
|
||||
export async function loadPassportDataAndSecret() {
|
||||
const passportData = await loadPassportData();
|
||||
const secret = await unsafe_getPrivateKey();
|
||||
if (!secret || !passportData) {
|
||||
return false;
|
||||
}
|
||||
return JSON.stringify({
|
||||
secret,
|
||||
passportData: JSON.parse(passportData),
|
||||
});
|
||||
}
|
||||
|
||||
export async function storePassportData(passportData: PassportData) {
|
||||
await Keychain.setGenericPassword(
|
||||
'passportData',
|
||||
JSON.stringify(passportData),
|
||||
{ service: 'passportData' },
|
||||
);
|
||||
}
|
||||
|
||||
export async function clearPassportData() {
|
||||
async function clearPassportDataFromKeychain() {
|
||||
await Keychain.resetGenericPassword({ service: 'passportData' });
|
||||
}
|
||||
|
||||
async function restoreFromMnemonic(mnemonic: string) {
|
||||
if (!mnemonic || !ethers.Mnemonic.isValidMnemonic(mnemonic)) {
|
||||
throw new Error('Invalid mnemonic');
|
||||
}
|
||||
|
||||
const restoredWallet = ethers.Wallet.fromPhrase(mnemonic);
|
||||
const data = JSON.stringify(restoredWallet.mnemonic);
|
||||
await Keychain.setGenericPassword('secret', data, {
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
return restoredWallet.mnemonic;
|
||||
}
|
||||
async function unsafe_clearSecrets() {
|
||||
if (__DEV__) {
|
||||
await Keychain.resetGenericPassword({ service: SERVICE_NAME });
|
||||
}
|
||||
}
|
||||
|
||||
interface PassportProviderProps extends PropsWithChildren {
|
||||
authenticationTimeoutinMs?: number;
|
||||
}
|
||||
|
||||
type Status = 'idle' | 'initializing' | 'updating' | 'error' | 'success';
|
||||
interface IPassportContext {
|
||||
getData: () => Promise<{ signature: string; data: PassportData } | null>;
|
||||
setData: (data: PassportData) => Promise<void>;
|
||||
getPassportDataAndSecret: () => Promise<{
|
||||
data: { passportData: PassportData; secret: string };
|
||||
signature: string;
|
||||
} | null>;
|
||||
passportData: PassportData | null;
|
||||
secret: Mnemonic | null;
|
||||
status: Status;
|
||||
unsafe_secret_privateKey?: string;
|
||||
setPassportData: (data: PassportData) => Promise<void>;
|
||||
clearPassportData: () => Promise<void>;
|
||||
setSecret: () => Promise<Mnemonic | null>;
|
||||
restorefromSecret: (mnemonic: string) => Promise<Mnemonic | null>;
|
||||
unsafe_clearSecrets: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const PassportContext = createContext<IPassportContext>({
|
||||
getData: () => Promise.resolve(null),
|
||||
setData: storePassportData,
|
||||
getPassportDataAndSecret: () => Promise.resolve(null),
|
||||
clearPassportData: clearPassportData,
|
||||
const PassportContext = createContext<IPassportContext>({
|
||||
passportData: null,
|
||||
secret: null,
|
||||
status: 'idle',
|
||||
setPassportData: () => Promise.resolve(),
|
||||
clearPassportData: () => Promise.resolve(),
|
||||
setSecret: () => Promise.resolve(null),
|
||||
restorefromSecret: () => Promise.resolve(null),
|
||||
unsafe_clearSecrets: () => Promise.resolve(),
|
||||
});
|
||||
|
||||
export const PassportProvider = ({ children }: PassportProviderProps) => {
|
||||
const { _getSecurely } = useAuth();
|
||||
const [status, setStatus] = useState<Status>('idle');
|
||||
const [passportCache, setPasspotCache] = useState<PassportData | null>(null);
|
||||
const [secretCache, setSecretCache] = useState<Mnemonic | null>(null);
|
||||
const [unsafePrivateKey, setUnsafePrivateKey] = useState<string>();
|
||||
const getPassportDataFromKeychain = useCallback(async () => {
|
||||
const passportDataCreds = await Keychain.getGenericPassword({
|
||||
service: 'passportData',
|
||||
});
|
||||
if (!passportDataCreds) {
|
||||
return false;
|
||||
}
|
||||
return JSON.parse(passportDataCreds.password);
|
||||
}, []);
|
||||
|
||||
const getData = useCallback(
|
||||
() => _getSecurely<PassportData>(loadPassportData, str => JSON.parse(str)),
|
||||
[_getSecurely],
|
||||
);
|
||||
const getSecretDataFromKeyChain = useCallback(async () => {
|
||||
const storedMnemonic = await Keychain.getGenericPassword({
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
if (storedMnemonic) {
|
||||
const parsed = JSON.parse(storedMnemonic.password);
|
||||
console.log('Stored mnemonic parsed successfully');
|
||||
return parsed as Mnemonic;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getPassportDataAndSecret = useCallback(
|
||||
() =>
|
||||
_getSecurely<{ passportData: PassportData; secret: string }>(
|
||||
loadPassportDataAndSecret,
|
||||
str => JSON.parse(str),
|
||||
),
|
||||
[_getSecurely],
|
||||
);
|
||||
const isPassportNull = useMemo(() => !passportCache, [passportCache]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setStatus(isPassportNull ? 'initializing' : 'updating');
|
||||
try {
|
||||
const passportData = await getPassportDataFromKeychain();
|
||||
if (passportData) {
|
||||
setPasspotCache(passportData);
|
||||
}
|
||||
const secret =
|
||||
(await getSecretDataFromKeyChain()) || (await setSecret());
|
||||
if (secret) {
|
||||
setSecretCache(secret);
|
||||
}
|
||||
setStatus('success');
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error fetching passport data or secret from keychain:',
|
||||
error,
|
||||
);
|
||||
setStatus('error');
|
||||
}
|
||||
})();
|
||||
}, [getPassportDataFromKeychain, getSecretDataFromKeyChain, isPassportNull]);
|
||||
|
||||
const setPassportData = useCallback(async (data: PassportData) => {
|
||||
await storePassportDataInKeychain(data);
|
||||
setPasspotCache(data);
|
||||
}, []);
|
||||
|
||||
const setSecret = useCallback(async () => {
|
||||
const { mnemonic, privateKey } = ethers.HDNodeWallet.fromMnemonic(
|
||||
ethers.Mnemonic.fromEntropy(ethers.randomBytes(32)),
|
||||
);
|
||||
setUnsafePrivateKey(privateKey);
|
||||
const data = JSON.stringify(mnemonic);
|
||||
await Keychain.setGenericPassword('secret', data, {
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
setSecretCache(mnemonic);
|
||||
return mnemonic;
|
||||
}, []);
|
||||
|
||||
const clearPassportData = useCallback(async () => {
|
||||
await clearPassportDataFromKeychain();
|
||||
setPasspotCache(null);
|
||||
}, []);
|
||||
|
||||
const restorefromSecret = useCallback(async (mnemonic: string) => {
|
||||
const data = await restoreFromMnemonic(mnemonic);
|
||||
setSecretCache(data);
|
||||
return data;
|
||||
}, []);
|
||||
|
||||
const state: IPassportContext = useMemo(
|
||||
() => ({
|
||||
getData,
|
||||
setData: storePassportData,
|
||||
getPassportDataAndSecret,
|
||||
clearPassportData: clearPassportData,
|
||||
passportData: passportCache,
|
||||
secret: secretCache,
|
||||
status,
|
||||
setPassportData,
|
||||
clearPassportData,
|
||||
restorefromSecret,
|
||||
setSecret,
|
||||
unsafe_clearSecrets,
|
||||
unsafe_secret_privateKey: unsafePrivateKey,
|
||||
}),
|
||||
[getData, getPassportDataAndSecret],
|
||||
[
|
||||
passportCache,
|
||||
secretCache,
|
||||
status,
|
||||
setPassportData,
|
||||
clearPassportData,
|
||||
restorefromSecret,
|
||||
setSecret,
|
||||
unsafePrivateKey,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -97,6 +190,18 @@ export const PassportProvider = ({ children }: PassportProviderProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const usePassport = () => {
|
||||
return useContext(PassportContext);
|
||||
export const usePassport = (auth = true) => {
|
||||
const c = useContext(PassportContext);
|
||||
if (!c) {
|
||||
throw new Error('usePassport must be used within a PassportProvider');
|
||||
}
|
||||
|
||||
const { isAuthenticated, isAuthenticating, loginWithBiometrics } = useAuth();
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated && !isAuthenticating && auth) {
|
||||
loginWithBiometrics();
|
||||
}
|
||||
}, [isAuthenticated, loginWithBiometrics, auth, isAuthenticating]);
|
||||
|
||||
return c;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import React, {
|
||||
|
||||
import { SelfApp } from '../../../common/src/utils/appType';
|
||||
import { setupUniversalLinkListener } from '../utils/qrCodeNew';
|
||||
import { usePassport } from './passportDataProvider';
|
||||
|
||||
export enum ProofStatusEnum {
|
||||
PENDING = 'pending',
|
||||
@@ -60,6 +61,7 @@ export let globalSetDisclosureStatus:
|
||||
store to manage the proof verification process, including app the is requesting, intemidiate status and final result
|
||||
*/
|
||||
export function ProofProvider({ children }: PropsWithChildren<{}>) {
|
||||
const { passportData, secret } = usePassport(false);
|
||||
const [registrationStatus, setRegistrationStatus] = useState<ProofStatusEnum>(
|
||||
ProofStatusEnum.PENDING,
|
||||
);
|
||||
@@ -97,14 +99,16 @@ export function ProofProvider({ children }: PropsWithChildren<{}>) {
|
||||
globalSetRegistrationStatus = null;
|
||||
globalSetDisclosureStatus = null;
|
||||
};
|
||||
}, [setRegistrationStatus, setDisclosureStatus]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const universalLinkCleanup = setupUniversalLinkListener(setSelectedApp);
|
||||
return () => {
|
||||
universalLinkCleanup();
|
||||
};
|
||||
}, []);
|
||||
if (passportData && secret) {
|
||||
const universalLinkCleanup = setupUniversalLinkListener(setSelectedApp);
|
||||
return () => {
|
||||
universalLinkCleanup();
|
||||
};
|
||||
}
|
||||
}, [passportData, secret, setSelectedApp]);
|
||||
|
||||
const publicApi: IProofContext = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -5,8 +5,6 @@ import { createJSONStorage, persist } from 'zustand/middleware';
|
||||
interface SettingsState {
|
||||
hasPrivacyNoteBeenDismissed: boolean;
|
||||
dismissPrivacyNote: () => void;
|
||||
biometricsAvailable: boolean;
|
||||
setBiometricsAvailable: (biometricsAvailable: boolean) => void;
|
||||
cloudBackupEnabled: boolean;
|
||||
toggleCloudBackupEnabled: () => void;
|
||||
isDevMode: boolean;
|
||||
@@ -23,12 +21,6 @@ export const useSettingStore = create<SettingsState>()(
|
||||
hasPrivacyNoteBeenDismissed: false,
|
||||
dismissPrivacyNote: () => set({ hasPrivacyNoteBeenDismissed: true }),
|
||||
|
||||
biometricsAvailable: false,
|
||||
setBiometricsAvailable: biometricsAvailable =>
|
||||
set({
|
||||
biometricsAvailable,
|
||||
}),
|
||||
|
||||
cloudBackupEnabled: false,
|
||||
toggleCloudBackupEnabled: () =>
|
||||
set(oldState => ({
|
||||
|
||||
@@ -4,36 +4,27 @@ import { decode } from 'msgpack-lite';
|
||||
import { inflate } from 'pako';
|
||||
|
||||
import { SelfApp } from '../../../common/src/utils/appType';
|
||||
import { loadPassportData } from '../stores/passportDataProvider';
|
||||
|
||||
export default async function handleQRCodeScan(
|
||||
result: string,
|
||||
setApp: (app: SelfApp) => void,
|
||||
) {
|
||||
try {
|
||||
const passportData = await loadPassportData();
|
||||
if (passportData) {
|
||||
const decodedResult = atob(result);
|
||||
const uint8Array = new Uint8Array(
|
||||
decodedResult.split('').map(char => char.charCodeAt(0)),
|
||||
);
|
||||
const decompressedData = inflate(uint8Array);
|
||||
const unpackedData = decode(decompressedData);
|
||||
const openPassportApp: SelfApp = unpackedData;
|
||||
const decodedResult = atob(result);
|
||||
const uint8Array = new Uint8Array(
|
||||
decodedResult.split('').map(char => char.charCodeAt(0)),
|
||||
);
|
||||
const decompressedData = inflate(uint8Array);
|
||||
const unpackedData = decode(decompressedData);
|
||||
const openPassportApp: SelfApp = unpackedData;
|
||||
|
||||
setApp(openPassportApp);
|
||||
console.log('✅', {
|
||||
message: 'QR code scanned',
|
||||
customData: {
|
||||
type: 'success',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log('Welcome', {
|
||||
message: 'Please register your passport first',
|
||||
type: 'info',
|
||||
});
|
||||
}
|
||||
setApp(openPassportApp);
|
||||
console.log('✅', {
|
||||
message: 'QR code scanned',
|
||||
customData: {
|
||||
type: 'success',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing QR code result:', error);
|
||||
console.log('Try again', {
|
||||
|
||||
Reference in New Issue
Block a user