Fix: recovery phrase (#201)

This commit is contained in:
Nicolas Brugneaux
2025-02-21 22:49:38 +01:00
committed by GitHub
parent c2d90fb5ad
commit 2155bf95a5
16 changed files with 379 additions and 150 deletions

View File

@@ -10,6 +10,7 @@ import { TamaguiProvider, createTamagui } from 'tamagui';
import App from './App';
import { name as appName } from './app.json';
import './src/utils/ethers';
const tamaguiConfig = createTamagui(config);

View File

@@ -68,6 +68,7 @@
"react-native-localize": "^3.4.1",
"react-native-nfc-manager": "^3.15.1",
"react-native-passport-reader": "^1.0.3",
"react-native-quick-crypto": "^0.7.12",
"react-native-safe-area-context": "^5.2.0",
"react-native-screens": "^4.6.0",
"react-native-svg": "^15.11.1",

View File

@@ -0,0 +1,24 @@
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,
};
}

View File

@@ -13,7 +13,7 @@ import RestoreAccountSvg from '../../images/icons/restore_account.svg';
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
import { useAuth } from '../../stores/authProvider';
import { useSettingStore } from '../../stores/settingStore';
import { STORAGE_NAME, useBackupPrivateKey } from '../../utils/cloudBackup';
import { STORAGE_NAME, useBackupMnemonic } from '../../utils/cloudBackup';
import { black, slate500, slate600, white } from '../../utils/colors';
interface AccountRecoveryChoiceScreenProps {}
@@ -21,11 +21,11 @@ interface AccountRecoveryChoiceScreenProps {}
const AccountRecoveryChoiceScreen: React.FC<
AccountRecoveryChoiceScreenProps
> = ({}) => {
const { restoreAccountFromPrivateKey } = useAuth();
const { restoreAccountFromMnemonic } = useAuth();
const [restoring, setRestoring] = useState(false);
const { cloudBackupEnabled, toggleCloudBackupEnabled, biometricsAvailable } =
useSettingStore();
const { download } = useBackupPrivateKey();
const { download } = useBackupMnemonic();
const onRestoreFromCloudNext = useHapticNavigation('AccountVerifiedSuccess');
const onEnterRecoveryPress = useHapticNavigation('RecoverWithPhrase');
@@ -33,8 +33,8 @@ const AccountRecoveryChoiceScreen: React.FC<
const onRestoreFromCloudPress = useCallback(async () => {
setRestoring(true);
try {
const restoredPrivKey = await download();
await restoreAccountFromPrivateKey(restoredPrivKey);
const mnemonic = await download();
await restoreAccountFromMnemonic(mnemonic.phrase);
if (!cloudBackupEnabled) {
toggleCloudBackupEnabled();
}
@@ -47,7 +47,7 @@ const AccountRecoveryChoiceScreen: React.FC<
}, [
cloudBackupEnabled,
download,
restoreAccountFromPrivateKey,
restoreAccountFromMnemonic,
onRestoreFromCloudNext,
]);
@@ -77,7 +77,8 @@ const AccountRecoveryChoiceScreen: React.FC<
onPress={onRestoreFromCloudPress}
disabled={restoring || !biometricsAvailable}
>
Restore from {STORAGE_NAME}
{restoring ? 'Restoring' : 'Restore'} from {STORAGE_NAME}
{restoring ? '' : ''}
</PrimaryButton>
<XStack gap={64} ai="center" justifyContent="space-between">
<Separator flexGrow={1} />

View File

@@ -45,6 +45,7 @@ const RecoverWithPhraseScreen: React.FC<
return;
}
const result = await restoreAccountFromMnemonic(slimMnemonic);
if (!result) {
console.warn('Failed to restore account');
// TODO SOMETHING ELSE?
@@ -52,7 +53,7 @@ const RecoverWithPhraseScreen: React.FC<
return;
}
setRestoring(false);
navigation.navigate('Home');
navigation.navigate('AccountVerifiedSuccess');
}, [mnemonic, restoreAccountFromMnemonic]);
return (

View File

@@ -1,7 +1,4 @@
import React, { useCallback, useState } from 'react';
import { findBestLanguageTag } from 'react-native-localize';
import { ethers } from 'ethers';
import Mnemonic from '../../components/Mnemonic';
import { PrimaryButton } from '../../components/buttons/PrimaryButton';
@@ -10,8 +7,8 @@ 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 { useAuth } from '../../stores/authProvider';
import { STORAGE_NAME } from '../../utils/cloudBackup';
import { black, slate400, white } from '../../utils/colors';
@@ -20,33 +17,14 @@ interface SaveRecoveryPhraseScreenProps {}
const SaveRecoveryPhraseScreen: React.FC<
SaveRecoveryPhraseScreenProps
> = ({}) => {
const { getOrCreatePrivateKey } = useAuth();
const [mnemonic, setMnemonic] = useState<string[]>();
const [userHasSeenMnemonic, setUserHasSeenMnemonic] = useState(false);
const { mnemonic, loadMnemonic } = useMnemonic();
const onRevealWords = useCallback(async () => {
await loadMnemonic();
setUserHasSeenMnemonic(true);
}, []);
const loadMnemonic = useCallback(async () => {
const privKey = await getOrCreatePrivateKey();
if (!privKey) {
return;
}
const { languageTag } = findBestLanguageTag(
Object.keys(ethers.wordlists),
) || { languageTag: 'en' };
const words = ethers.Mnemonic.entropyToPhrase(
privKey.data,
ethers.wordlists[languageTag],
);
setMnemonic(words.trim().split(' '));
}, []);
const onCloudBackupPress = useHapticNavigation('CloudBackupSettings', {
params: { nextScreen: 'SaveRecoveryPhrase' },
});

View File

@@ -15,7 +15,7 @@ import Cloud from '../../images/icons/logo_cloud_backup.svg';
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
import { useAuth } from '../../stores/authProvider';
import { useSettingStore } from '../../stores/settingStore';
import { STORAGE_NAME, useBackupPrivateKey } from '../../utils/cloudBackup';
import { STORAGE_NAME, useBackupMnemonic } from '../../utils/cloudBackup';
import { black, white } from '../../utils/colors';
import { buttonTap, confirmTap } from '../../utils/haptic';
@@ -32,10 +32,10 @@ interface CloudBackupScreenProps
const CloudBackupScreen: React.FC<CloudBackupScreenProps> = ({
route: { params },
}) => {
const { getOrCreatePrivateKey, loginWithBiometrics } = useAuth();
const { getOrCreateMnemonic, loginWithBiometrics } = useAuth();
const { cloudBackupEnabled, toggleCloudBackupEnabled, biometricsAvailable } =
useSettingStore();
const { upload, disableBackup } = useBackupPrivateKey();
const { upload, disableBackup } = useBackupMnemonic();
const [pending, setPending] = useState(false);
const { showModal } = useModal(
@@ -70,17 +70,17 @@ const CloudBackupScreen: React.FC<CloudBackupScreenProps> = ({
setPending(true);
const privKey = await getOrCreatePrivateKey();
if (!privKey) {
const storedMnemonic = await getOrCreateMnemonic();
if (!storedMnemonic) {
setPending(false);
return;
}
await upload(privKey.data);
await upload(storedMnemonic.data);
toggleCloudBackupEnabled();
setPending(false);
}, [
cloudBackupEnabled,
getOrCreatePrivateKey,
getOrCreateMnemonic,
upload,
toggleCloudBackupEnabled,
]);

View File

@@ -1,4 +1,5 @@
import React from 'react';
import React, { PropsWithChildren, useEffect, useState } from 'react';
import { Platform, TextInput } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import {
@@ -8,10 +9,23 @@ import {
IterationCw,
VenetianMask,
} from '@tamagui/lucide-icons';
import { Adapt, Button, Fieldset, Label, Select, Sheet, YStack } from 'tamagui';
import {
Adapt,
Button,
Fieldset,
Label,
Select,
Sheet,
Text,
YStack,
} from 'tamagui';
import { genMockPassportData } from '../../../../common/src/utils/passports/genMockPassportData';
import { RootStackParamList } from '../../Navigation';
import {
unsafe_clearSecrets,
unsafe_getPrivateKey,
} from '../../stores/authProvider';
import {
storePassportData,
usePassport,
@@ -20,6 +34,22 @@ import { borderColor, textBlack } from '../../utils/colors';
interface DevSettingsScreenProps {}
function SelectableText({ children, ...props }: PropsWithChildren) {
if (Platform.OS === 'ios') {
return (
<TextInput multiline editable={false} {...props}>
{children}
</TextInput>
);
} else {
return (
<Text selectable {...props}>
{children}
</Text>
);
}
}
const items = [
'DevSettings',
'Splash',
@@ -103,14 +133,20 @@ const ScreenSelector = ({}) => {
const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
const { clearPassportData } = usePassport();
const [privateKey, setPrivateKey] = useState('Loading private key…');
const nav = useNavigation();
function handleRestart() {
clearPassportData();
async function handleRestart() {
await clearPassportData();
nav.navigate('Launch');
}
async function deleteEverything() {
await unsafe_clearSecrets();
await handleRestart();
}
function handleGenerateMockPassportData() {
const passportData = genMockPassportData(
'sha256',
@@ -123,15 +159,14 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
storePassportData(passportData);
}
useEffect(() => {
unsafe_getPrivateKey().then(setPrivateKey);
}, []);
return (
<YStack gap="$2" mt="$2" ai="center">
<Fieldset gap="$4" horizontal>
<Label
color={textBlack}
width={200}
justifyContent="flex-end"
htmlFor="restart"
>
<YStack gap="$3" mt="$2" ai="center">
<Fieldset px="$4" horizontal width="100%" justifyContent="space-between">
<Label color={textBlack} width={200} justifyContent="flex-end">
Rescan passport
</Label>
<Button
@@ -146,13 +181,8 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
<IterationCw color={textBlack} />
</Button>
</Fieldset>
<Fieldset gap="$4" horizontal>
<Label
color={textBlack}
width={200}
justifyContent="flex-end"
htmlFor="restart"
>
<Fieldset px="$4" horizontal width="100%" justifyContent="space-between">
<Label color={textBlack} width={200} justifyContent="flex-end">
Generate mock passport data
</Label>
<Button
@@ -168,13 +198,8 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
</Button>
</Fieldset>
<Fieldset gap="$4" mt="$1" horizontal marginBottom={30}>
<Label
color={textBlack}
width={200}
justifyContent="flex-end"
htmlFor="skip"
>
<Fieldset px="$4" horizontal width="100%" justifyContent="space-between">
<Label color={textBlack} width={200} justifyContent="flex-end">
Delete passport data
</Label>
<Button
@@ -184,18 +209,66 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
borderWidth={1.2}
size="$3.5"
ml="$2"
// onPress={}
onPress={clearPassportData}
>
<Eraser color={textBlack} />
</Button>
</Fieldset>
<Fieldset px="$4" horizontal width="100%" justifyContent="space-between">
<Label color={textBlack} width={200} justifyContent="flex-end">
Delete keychain secrets
</Label>
<Button
bg="white"
jc="center"
borderColor={borderColor}
borderWidth={1.2}
size="$3.5"
ml="$2"
onPress={unsafe_clearSecrets}
>
<Eraser color={textBlack} />
</Button>
</Fieldset>
<Fieldset marginTop={30} gap="$4" mt="$1" horizontal>
<Label color={textBlack} justifyContent="flex-end" htmlFor="skip">
Shortcut
<Fieldset px="$4" horizontal width="100%" justifyContent="space-between">
<Label color={textBlack} width={200} justifyContent="flex-end">
Delete everything
</Label>
<Button
bg="white"
jc="center"
borderColor={borderColor}
borderWidth={1.2}
size="$3.5"
ml="$2"
onPress={deleteEverything}
>
<Eraser color={textBlack} />
</Button>
</Fieldset>
<Fieldset px="$4" horizontal width="100%" justifyContent="space-between">
<Label color={textBlack} justifyContent="flex-end">
Shortcuts
</Label>
<ScreenSelector />
</Fieldset>
<Fieldset px="$4" width="100%" mt={30} justifyContent="space-between">
<Label color={textBlack} width={200} justifyContent="flex-end">
Private key
</Label>
<SelectableText
color={textBlack}
width={300}
justifyContent="flex-end"
userSelect="all"
style={{ fontFamily: 'monospace', fontWeight: 'bold' }}
>
{privateKey}
</SelectableText>
</Fieldset>
</YStack>
);
};

View File

@@ -1,42 +1,21 @@
import React, { useCallback, useState } from 'react';
import { findBestLanguageTag } from 'react-native-localize';
import { ethers } from 'ethers';
import React, { useCallback } from 'react';
import Mnemonic from '../../components/Mnemonic';
import Description from '../../components/typography/Description';
import useMnemonic from '../../hooks/useMnemonic';
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
import { useAuth } from '../../stores/authProvider';
interface ShowRecoveryPhraseScreenProps {}
const ShowRecoveryPhraseScreen: React.FC<
ShowRecoveryPhraseScreenProps
> = ({}) => {
const { getOrCreatePrivateKey } = useAuth();
const [mnemonic, setMnemonic] = useState<string[]>();
const { mnemonic, loadMnemonic } = useMnemonic();
const onRevealWords = useCallback(async () => {
await loadMnemonic();
}, []);
const loadMnemonic = useCallback(async () => {
const privKey = await getOrCreatePrivateKey();
if (!privKey) {
return;
}
const { languageTag } = findBestLanguageTag(
Object.keys(ethers.wordlists),
) || { languageTag: 'en' };
const words = ethers.Mnemonic.entropyToPhrase(
privKey.data,
ethers.wordlists[languageTag],
);
setMnemonic(words.trim().split(' '));
}, []);
return (
<ExpandableBottomLayout.Layout backgroundColor="white">
<ExpandableBottomLayout.BottomSection

View File

@@ -5,7 +5,7 @@ import { useNavigation } from '@react-navigation/native';
import LottieView from 'lottie-react-native';
import splashAnimation from '../assets/animations/splash.json';
import { loadSecret, useAuth } from '../stores/authProvider';
import { hasSecretStored, useAuth } from '../stores/authProvider';
import { loadPassportData } from '../stores/passportDataProvider';
import { useSettingStore } from '../stores/settingStore';
import { black } from '../utils/colors';
@@ -30,7 +30,7 @@ const SplashScreen: React.FC = ({}) => {
const handleAnimationFinish = useCallback(() => {
setTimeout(async () => {
impactLight();
const secret = await loadSecret();
const secret = await hasSecretStored();
const passportData = await loadPassportData();
if (secret && passportData) {
@@ -39,7 +39,7 @@ const SplashScreen: React.FC = ({}) => {
navigation.navigate('Launch');
}
}, 1000);
}, [loadSecret, loadPassportData, navigation]);
}, [navigation]);
return (
<LottieView

View File

@@ -11,6 +11,10 @@ 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>,
@@ -69,34 +73,36 @@ async function createSigningKeyPair(): Promise<boolean> {
}
}
export async function loadSecret() {
const secretCreds = await Keychain.getGenericPassword({ service: 'secret' });
return secretCreds === false ? false : secretCreds.password;
}
async function restoreFromMnemonic(mnemonic: string) {
if (!mnemonic || !ethers.Mnemonic.isValidMnemonic(mnemonic)) {
throw new Error('Invalid mnemonic');
}
const restoredWallet = ethers.Wallet.fromPhrase(mnemonic);
return restoreFromPrivateKey(restoredWallet.privateKey);
}
async function restoreFromPrivateKey(privateKey: string) {
await Keychain.setGenericPassword('secret', privateKey, {
service: 'secret',
const data = JSON.stringify(restoredWallet.mnemonic);
await Keychain.setGenericPassword('secret', data, {
service: SERVICE_NAME,
});
return loadSecret();
return data;
}
export async function loadSecretOrCreateIt() {
const secret = await loadSecret();
if (secret) {
return secret;
async function loadOrCreateMnemonic() {
const storedMnemonic = await Keychain.getGenericPassword({
service: SERVICE_NAME,
});
if (storedMnemonic) {
return storedMnemonic.password;
}
console.log('No secret found, creating one');
const randomWallet = ethers.Wallet.createRandom();
const newSecret = randomWallet.privateKey;
await Keychain.setGenericPassword('secret', newSecret, { service: 'secret' });
return newSecret;
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({
@@ -110,13 +116,10 @@ interface IAuthContext {
isAuthenticating: boolean;
loginWithBiometrics: () => Promise<void>;
_getSecurely: typeof _getSecurely;
getOrCreatePrivateKey: () => Promise<SignedPayload<string> | null>;
getOrCreateMnemonic: () => Promise<SignedPayload<Mnemonic> | null>;
restoreAccountFromMnemonic: (
mnemonic: string,
) => Promise<SignedPayload<string> | null>;
restoreAccountFromPrivateKey: (
privKey: string,
) => Promise<SignedPayload<string> | null>;
) => Promise<SignedPayload<boolean> | null>;
createSigningKeyPair: () => Promise<boolean>;
}
export const AuthContext = createContext<IAuthContext>({
@@ -124,9 +127,8 @@ export const AuthContext = createContext<IAuthContext>({
isAuthenticating: false,
loginWithBiometrics: () => Promise.resolve(),
_getSecurely,
getOrCreatePrivateKey: () => Promise.resolve(null),
getOrCreateMnemonic: () => Promise.resolve(null),
restoreAccountFromMnemonic: () => Promise.resolve(null),
restoreAccountFromPrivateKey: () => Promise.resolve(null),
createSigningKeyPair: () => Promise.resolve(false),
});
@@ -174,24 +176,16 @@ export const AuthProvider = ({
});
}, [isAuthenticatingPromise]);
const getOrCreatePrivateKey = useCallback(
() => _getSecurely<string>(loadSecretOrCreateIt, str => str),
const getOrCreateMnemonic = useCallback(
() => _getSecurely<Mnemonic>(loadOrCreateMnemonic, str => JSON.parse(str)),
[],
);
const restoreAccountFromMnemonic = useCallback(
(mnemonic: string) =>
_getSecurely<string>(
_getSecurely<boolean>(
() => restoreFromMnemonic(mnemonic),
str => str,
),
[],
);
const restoreAccountFromPrivateKey = useCallback(
(privKey: string) =>
_getSecurely<string>(
() => restoreFromPrivateKey(privKey),
str => str,
str => !!str,
),
[],
);
@@ -201,9 +195,8 @@ export const AuthProvider = ({
isAuthenticated,
isAuthenticating: !!isAuthenticatingPromise,
loginWithBiometrics,
getOrCreatePrivateKey,
getOrCreateMnemonic,
restoreAccountFromMnemonic,
restoreAccountFromPrivateKey,
createSigningKeyPair,
_getSecurely,
}),
@@ -216,3 +209,24 @@ 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 });
}
}

View File

@@ -8,9 +8,10 @@ import React, {
import Keychain from 'react-native-keychain';
import { PassportData } from '../../../common/src/utils/types';
import { loadSecretOrCreateIt } from '../stores/authProvider';
import { unsafe_getPrivateKey } from '../stores/authProvider';
import { useAuth } from './authProvider';
// TODO: refactor this as it shouldnt be used directly IMHO
export async function loadPassportData() {
const passportDataCreds = await Keychain.getGenericPassword({
service: 'passportData',
@@ -20,7 +21,7 @@ export async function loadPassportData() {
export async function loadPassportDataAndSecret() {
const passportData = await loadPassportData();
const secret = await loadSecretOrCreateIt();
const secret = await unsafe_getPrivateKey();
if (!secret || !passportData) {
return false;
}

26
app/src/types/mnemonic.ts Normal file
View File

@@ -0,0 +1,26 @@
export interface Mnemonic {
/**
* The mnemonic phrase of 12, 15, 18, 21 or 24 words.
*
* Use the [[wordlist]] ``split`` method to get the individual words.
*/
readonly phrase: string;
/**
* The password used for this mnemonic. If no password is used this
* is the empty string (i.e. ``""``) as per the specification.
*/
readonly password: string;
/**
* The wordlist for this mnemonic.
*/
readonly wordlist: {
readonly locale: string;
};
/**
* The underlying entropy which the mnemonic encodes.
*/
readonly entropy: string;
}

View File

@@ -6,7 +6,10 @@ import {
CloudStorageScope,
} from 'react-native-cloud-storage';
import { ethers } from 'ethers';
import { name } from '../../../package.json';
import { Mnemonic } from '../../types/mnemonic';
import { googleSignIn } from './google';
const FOLDER = `/${name}`;
@@ -44,7 +47,7 @@ async function withRetries<T>(
);
}
export function useBackupPrivateKey() {
export function useBackupMnemonic() {
return useMemo(
() => ({
upload,
@@ -68,10 +71,10 @@ async function addAccessTokenForGoogleDrive() {
}
}
async function upload(privateKey: string) {
if (!privateKey) {
async function upload(mnemonic: Mnemonic) {
if (!mnemonic || !mnemonic.phrase) {
throw new Error(
'Private key not set yet. Did the user see the recovery phrase?',
'Mnemonic not set yet. Did the user see the recovery phrase?',
);
}
@@ -85,17 +88,31 @@ async function upload(privateKey: string) {
}
}
await withRetries(() =>
CloudStorage.writeFile(ENCRYPTED_FILE_PATH, privateKey),
CloudStorage.writeFile(ENCRYPTED_FILE_PATH, JSON.stringify(mnemonic)),
);
}
async function download() {
await addAccessTokenForGoogleDrive();
if (await CloudStorage.exists(ENCRYPTED_FILE_PATH)) {
const privateKey = await withRetries(() =>
const mnemonicString = await withRetries(() =>
CloudStorage.readFile(ENCRYPTED_FILE_PATH),
);
return privateKey;
try {
const mnemonic = JSON.parse(mnemonicString) as Mnemonic;
if (
!mnemonic.phrase ||
!ethers.Mnemonic.isValidMnemonic(mnemonic.phrase)
) {
throw new Error();
}
return mnemonic;
} catch (e) {
throw new Error(
`Malformed mnemonic, expected JSON structure, got ${mnemonicString}`,
);
}
}
throw new Error(
'Couldnt find the encrypted backup, did you back it up previously?',

27
app/src/utils/ethers.ts Normal file
View File

@@ -0,0 +1,27 @@
// https://docs.ethers.org/v6/cookbook/react-native/
import crypto from 'react-native-quick-crypto';
import { ethers } from 'ethers';
ethers.randomBytes.register(length => {
return new Uint8Array(crypto.randomBytes(length));
});
ethers.computeHmac.register((algo, key, data) => {
return crypto.createHmac(algo, key).update(data).digest();
});
// @ts-expect-error
ethers.pbkdf2.register((passwd, salt, iter, keylen, algo) => {
return crypto.pbkdf2Sync(passwd, salt, iter, keylen, algo);
});
ethers.sha256.register(data => {
// @ts-expect-error
return crypto.createHash('sha256').update(data).digest();
});
ethers.sha512.register(data => {
// @ts-expect-error
return crypto.createHash('sha512').update(data).digest();
});

View File

@@ -1387,6 +1387,16 @@ __metadata:
languageName: node
linkType: hard
"@craftzdog/react-native-buffer@npm:^6.0.5":
version: 6.0.5
resolution: "@craftzdog/react-native-buffer@npm:6.0.5"
dependencies:
ieee754: "npm:^1.2.1"
react-native-quick-base64: "npm:^2.0.5"
checksum: 10c0/76923a9d982055e64e8a6e01fd5cb1cc291d32a95fa259be641fa0fe93da22adaf2d98b9ae2b960cc8c0a78112ec24903c9d80ce4d31b87e5d99c3f656c46c0c
languageName: node
linkType: hard
"@egjs/hammerjs@npm:^2.0.17":
version: 2.0.17
resolution: "@egjs/hammerjs@npm:2.0.17"
@@ -8531,6 +8541,13 @@ __metadata:
languageName: node
linkType: hard
"events@npm:^3.3.0":
version: 3.3.0
resolution: "events@npm:3.3.0"
checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6
languageName: node
linkType: hard
"execa@npm:^5.0.0, execa@npm:^5.1.1":
version: 5.1.1
resolution: "execa@npm:5.1.1"
@@ -9394,6 +9411,16 @@ __metadata:
languageName: node
linkType: hard
"is-arguments@npm:^1.0.4":
version: 1.2.0
resolution: "is-arguments@npm:1.2.0"
dependencies:
call-bound: "npm:^1.0.2"
has-tostringtag: "npm:^1.0.2"
checksum: 10c0/6377344b31e9fcb707c6751ee89b11f132f32338e6a782ec2eac9393b0cbd32235dad93052998cda778ee058754860738341d8114910d50ada5615912bb929fc
languageName: node
linkType: hard
"is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5":
version: 3.0.5
resolution: "is-array-buffer@npm:3.0.5"
@@ -9540,7 +9567,7 @@ __metadata:
languageName: node
linkType: hard
"is-generator-function@npm:^1.0.10":
"is-generator-function@npm:^1.0.10, is-generator-function@npm:^1.0.7":
version: 1.1.0
resolution: "is-generator-function@npm:1.1.0"
dependencies:
@@ -9671,7 +9698,7 @@ __metadata:
languageName: node
linkType: hard
"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15":
"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15, is-typed-array@npm:^1.1.3":
version: 1.1.15
resolution: "is-typed-array@npm:1.1.15"
dependencies:
@@ -11976,6 +12003,7 @@ __metadata:
react-native-localize: "npm:^3.4.1"
react-native-nfc-manager: "npm:^3.15.1"
react-native-passport-reader: "npm:^1.0.3"
react-native-quick-crypto: "npm:^0.7.12"
react-native-safe-area-context: "npm:^5.2.0"
react-native-screens: "npm:^4.6.0"
react-native-svg: "npm:^15.11.1"
@@ -12335,6 +12363,13 @@ __metadata:
languageName: node
linkType: hard
"process@npm:^0.11.10":
version: 0.11.10
resolution: "process@npm:0.11.10"
checksum: 10c0/40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3
languageName: node
linkType: hard
"promise-retry@npm:^2.0.1":
version: 2.0.1
resolution: "promise-retry@npm:2.0.1"
@@ -12633,6 +12668,31 @@ __metadata:
languageName: node
linkType: hard
"react-native-quick-base64@npm:^2.0.5":
version: 2.1.2
resolution: "react-native-quick-base64@npm:2.1.2"
dependencies:
base64-js: "npm:^1.5.1"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10c0/716383251318f4eeeaec9845b9f6d3468bb9c9af58972a610d22a02961ccf839436d5fb7dac9cbb71275b7478ed6b766224d39ab20c122de3c6c70fa040d149f
languageName: node
linkType: hard
"react-native-quick-crypto@npm:^0.7.12":
version: 0.7.12
resolution: "react-native-quick-crypto@npm:0.7.12"
dependencies:
"@craftzdog/react-native-buffer": "npm:^6.0.5"
events: "npm:^3.3.0"
readable-stream: "npm:^4.5.2"
string_decoder: "npm:^1.3.0"
util: "npm:^0.12.5"
checksum: 10c0/acc7bf1da4f86845696a1afd3e4a3ac68eb3a333e65f6ab56499143e25fe13f435a86df3a2f6c32e9132e1f3ce0ba86d8d3e02876aff25ba2a30b6b2406d6b11
languageName: node
linkType: hard
"react-native-safe-area-context@npm:^5.2.0":
version: 5.2.0
resolution: "react-native-safe-area-context@npm:5.2.0"
@@ -12878,6 +12938,19 @@ __metadata:
languageName: node
linkType: hard
"readable-stream@npm:^4.5.2":
version: 4.7.0
resolution: "readable-stream@npm:4.7.0"
dependencies:
abort-controller: "npm:^3.0.0"
buffer: "npm:^6.0.3"
events: "npm:^3.3.0"
process: "npm:^0.11.10"
string_decoder: "npm:^1.3.0"
checksum: 10c0/fd86d068da21cfdb10f7a4479f2e47d9c0a9b0c862fc0c840a7e5360201580a55ac399c764b12a4f6fa291f8cee74d9c4b7562e0d53b3c4b2769f2c98155d957
languageName: node
linkType: hard
"readable-stream@npm:~2.3.6":
version: 2.3.8
resolution: "readable-stream@npm:2.3.8"
@@ -13883,7 +13956,7 @@ __metadata:
languageName: node
linkType: hard
"string_decoder@npm:^1.1.1":
"string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0":
version: 1.3.0
resolution: "string_decoder@npm:1.3.0"
dependencies:
@@ -14556,6 +14629,19 @@ __metadata:
languageName: node
linkType: hard
"util@npm:^0.12.5":
version: 0.12.5
resolution: "util@npm:0.12.5"
dependencies:
inherits: "npm:^2.0.3"
is-arguments: "npm:^1.0.4"
is-generator-function: "npm:^1.0.7"
is-typed-array: "npm:^1.1.3"
which-typed-array: "npm:^1.1.2"
checksum: 10c0/c27054de2cea2229a66c09522d0fa1415fb12d861d08523a8846bf2e4cbf0079d4c3f725f09dcb87493549bcbf05f5798dce1688b53c6c17201a45759e7253f3
languageName: node
linkType: hard
"utils-merge@npm:1.0.1":
version: 1.0.1
resolution: "utils-merge@npm:1.0.1"
@@ -14708,7 +14794,7 @@ __metadata:
languageName: node
linkType: hard
"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18":
"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2":
version: 1.1.18
resolution: "which-typed-array@npm:1.1.18"
dependencies: