adding hash gate to make sure accounts match up (#1003)
This commit is contained in:
@@ -34,6 +34,7 @@ import { WebViewProvider } from './context-store/webViewContext';
|
||||
import { Linking, Platform, NativeModules } from 'react-native';
|
||||
|
||||
import SplashScreen from './app/screens/splashScreen';
|
||||
import sha256Hash from './app/functions/hash';
|
||||
import { GlobalContactsList } from './context-store/globalContacts';
|
||||
|
||||
import { CreateAccountHome } from './app/screens/createAccount';
|
||||
@@ -497,11 +498,20 @@ function ResetStack(): JSX.Element | null {
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
if (mnemonic.value && !parsedSettings.isSecurityEnabled) {
|
||||
const isNoSecurityLogin =
|
||||
mnemonic.value && !parsedSettings.isSecurityEnabled;
|
||||
if (isNoSecurityLogin) {
|
||||
setAccountMnemonic(mnemonic.value);
|
||||
}
|
||||
|
||||
setSecuritySettings(parsedSettings);
|
||||
// For the no-security path the loading screen renders directly as Home, so
|
||||
// thread the intended seed's hash through its initialParams (kept out of the
|
||||
// persisted security-settings object) to gate its identity derivation.
|
||||
setSecuritySettings(
|
||||
isNoSecurityLogin
|
||||
? { ...parsedSettings, expectedMnemonicHash: sha256Hash(mnemonic.value) }
|
||||
: parsedSettings,
|
||||
);
|
||||
setInitSettings(prev => {
|
||||
return {
|
||||
...prev,
|
||||
|
||||
@@ -234,6 +234,7 @@ describe('BiometricsLogin - never deadlocks (RC1)', () => {
|
||||
expect(mockSetAccountMnemonic).toHaveBeenCalledWith('seed words');
|
||||
expect(mockNavigate.replace).toHaveBeenCalledWith(
|
||||
'ConnectingToNodeLoadingScreen',
|
||||
{ expectedMnemonicHash: 'HASHED' },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -252,6 +253,7 @@ describe('BiometricsLogin - auto-trigger', () => {
|
||||
expect(mockNavigate.replace).toHaveBeenCalledTimes(1);
|
||||
expect(mockNavigate.replace).toHaveBeenCalledWith(
|
||||
'ConnectingToNodeLoadingScreen',
|
||||
{ expectedMnemonicHash: 'HASHED' },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -398,6 +400,7 @@ describe('BiometricsLogin - legacy migration', () => {
|
||||
expect(mockSetAccountMnemonic).toHaveBeenCalledWith('LEGACY_MNEMONIC');
|
||||
expect(mockNavigate.replace).toHaveBeenCalledWith(
|
||||
'ConnectingToNodeLoadingScreen',
|
||||
{ expectedMnemonicHash: 'HASHED' },
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -143,7 +143,9 @@ export default function BiometricsLogin() {
|
||||
storeData('pinHash', sha256Hash(storedPin.value));
|
||||
setAccountMnemonic(savedMnemonic.value);
|
||||
didNavigate.current = true;
|
||||
navigate.replace('ConnectingToNodeLoadingScreen');
|
||||
navigate.replace('ConnectingToNodeLoadingScreen', {
|
||||
expectedMnemonicHash: sha256Hash(savedMnemonic.value),
|
||||
});
|
||||
} else {
|
||||
navigate.navigate('ConfirmActionPage', {
|
||||
confirmMessage: t(
|
||||
@@ -197,7 +199,9 @@ export default function BiometricsLogin() {
|
||||
if (decryptResponse) {
|
||||
setAccountMnemonic(decryptResponse);
|
||||
didNavigate.current = true;
|
||||
navigate.replace('ConnectingToNodeLoadingScreen');
|
||||
navigate.replace('ConnectingToNodeLoadingScreen', {
|
||||
expectedMnemonicHash: sha256Hash(decryptResponse),
|
||||
});
|
||||
} else {
|
||||
// Genuine failure/cancel — count it toward the retry limit.
|
||||
numRetriesBiometric.current++;
|
||||
|
||||
@@ -102,7 +102,9 @@ export default function PinPage() {
|
||||
if (migrationResponse) {
|
||||
setAccountMnemonic(savedMnemonic.value);
|
||||
didNavigate.current = true;
|
||||
navigate.replace('ConnectingToNodeLoadingScreen');
|
||||
navigate.replace('ConnectingToNodeLoadingScreen', {
|
||||
expectedMnemonicHash: sha256Hash(savedMnemonic.value),
|
||||
});
|
||||
} else
|
||||
navigate.navigate('ErrorScreen', {
|
||||
errorMessage: t('errormessages.failedToDecryptPin'),
|
||||
@@ -115,7 +117,9 @@ export default function PinPage() {
|
||||
setAccountMnemonic(mnemonicPlain);
|
||||
|
||||
didNavigate.current = true;
|
||||
navigate.replace('ConnectingToNodeLoadingScreen');
|
||||
navigate.replace('ConnectingToNodeLoadingScreen', {
|
||||
expectedMnemonicHash: sha256Hash(mnemonicPlain),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (loginSettings.enteredPinCount >= 7) {
|
||||
|
||||
@@ -13,12 +13,7 @@ import { CENTER, COLORS, SIZES } from '../../constants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GlobalThemeView, ThemeText } from '../../functions/CustomElements';
|
||||
import CustomButton from '../../functions/CustomElements/button';
|
||||
import { createAccountMnemonic } from '../../functions';
|
||||
import {
|
||||
crashlyticsLogReport,
|
||||
crashlyticsRecordErrorReport,
|
||||
} from '../../functions/crashlyticsLogs';
|
||||
import { useKeysContext } from '../../../context-store/keys';
|
||||
import { crashlyticsLogReport } from '../../functions/crashlyticsLogs';
|
||||
import {
|
||||
FONT,
|
||||
HIDDEN_OPACITY,
|
||||
@@ -96,7 +91,6 @@ const easeOut = Easing.out(Easing.cubic);
|
||||
// ─── Main ─────────────────────────────────────────────────────────────────────
|
||||
export default function CreateAccountHome({ navigation: { navigate } }) {
|
||||
const { t } = useTranslation();
|
||||
const { setAccountMnemonic } = useKeysContext();
|
||||
const { screenDimensions } = useAppStatus();
|
||||
|
||||
// Shared values
|
||||
@@ -148,18 +142,6 @@ export default function CreateAccountHome({ navigation: { navigate } }) {
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
crashlyticsLogReport('Creating account mnemonic');
|
||||
const mnemonic = await createAccountMnemonic();
|
||||
setAccountMnemonic(mnemonic);
|
||||
} catch (err) {
|
||||
crashlyticsRecordErrorReport(err.message);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const go = (page, nextPage) => {
|
||||
crashlyticsLogReport(`Navigating to ${page} from create account home`);
|
||||
navigate(page, { nextPage });
|
||||
|
||||
@@ -14,6 +14,7 @@ import { storeMnemonicWithPinSecurity } from '../../../functions/handleMnemonic'
|
||||
import { privateKeyFromSeedWords } from '../../../functions/nostrCompatability';
|
||||
import { getPublicKey } from 'nostr-tools';
|
||||
import { initializeFirebase } from '../../../../db/initializeFirebase';
|
||||
import sha256Hash from '../../../functions/hash';
|
||||
|
||||
export default function PinPage(props) {
|
||||
const { accountMnemoinc } = useKeysContext();
|
||||
@@ -27,6 +28,9 @@ export default function PinPage(props) {
|
||||
const didNavigate = useRef(null);
|
||||
// const fromGiftPath = props.route.params?.from === 'giftPath';
|
||||
const didRestoreWallet = props.route.params?.didRestoreWallet;
|
||||
// For restore this is the hash of the seed the user typed (source of truth).
|
||||
// For create it's undefined and we hash the committed context seed below.
|
||||
const restoreExpectedHash = props.route.params?.expectedMnemonicHash;
|
||||
|
||||
useEffect(() => {
|
||||
// begin initializing firebase to speed up loading time
|
||||
@@ -83,6 +87,13 @@ export default function PinPage(props) {
|
||||
routes: [
|
||||
{
|
||||
name: 'ConnectingToNodeLoadingScreen',
|
||||
params: {
|
||||
// Pin the loading screen's identity derivation to the exact seed
|
||||
// just stored, so it can't derive from a stale/empty context seed
|
||||
// during the navigation race.
|
||||
expectedMnemonicHash:
|
||||
restoreExpectedHash || sha256Hash(accountMnemoinc),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '../../../constants';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import isValidMnemonic from '../../../functions/isValidMnemonic';
|
||||
import sha256Hash from '../../../functions/hash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
CustomKeyboardAvoidingView,
|
||||
@@ -188,8 +189,16 @@ export default function RestoreWallet({
|
||||
if (!hasAccount)
|
||||
throw new Error(t('createAccount.restoreWallet.home.error2'));
|
||||
else {
|
||||
setAccountMnemonic(mnemonic.join(' '));
|
||||
navigate.navigate('PinSetup', { didRestoreWallet: true });
|
||||
const restoredSeed = mnemonic.join(' ');
|
||||
setAccountMnemonic(restoredSeed);
|
||||
// Thread the hash of the seed the user actually entered through to the
|
||||
// loading screen (via PinSetup). This is the source of truth for the
|
||||
// account identity — immune to any later clobber of the shared
|
||||
// accountMnemoinc context.
|
||||
navigate.navigate('PinSetup', {
|
||||
didRestoreWallet: true,
|
||||
expectedMnemonicHash: sha256Hash(restoredSeed),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('key validation error', err);
|
||||
|
||||
@@ -8,7 +8,11 @@ import { useGlobalContactsInfo } from '../../../context-store/globalContacts';
|
||||
import { useGlobalAppData } from '../../../context-store/appData';
|
||||
import { GlobalThemeView } from '../../functions/CustomElements';
|
||||
import LottieView from 'lottie-react-native';
|
||||
import { StackActions, useNavigation } from '@react-navigation/native';
|
||||
import {
|
||||
StackActions,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from '@react-navigation/native';
|
||||
import { navigationRef } from '../../../navigation/navigationService';
|
||||
import { useGlobalThemeContext } from '../../../context-store/theme';
|
||||
import { useKeysContext } from '../../../context-store/keys';
|
||||
@@ -23,6 +27,7 @@ import { useWebView } from '../../../context-store/webViewContext';
|
||||
import ThemeIcon from '../../functions/CustomElements/themeIcon';
|
||||
import { getCachedSparkTransactions } from '../../functions/spark';
|
||||
import { deriveSparkIdentityKey } from '../../functions/gift/deriveGiftWallet';
|
||||
import sha256Hash from '../../functions/hash';
|
||||
import { useAppStatus } from '../../../context-store/appStatus';
|
||||
import { initializeAllDatabases } from '../../functions/initializeAllDatabases';
|
||||
import openWebBrowser from '../../functions/openWebBrowser';
|
||||
@@ -34,6 +39,13 @@ const mascotAnimation = require('../../assets/MOSCATWALKING.json');
|
||||
|
||||
export default function ConnectingToNodeLoadingScreen() {
|
||||
const navigate = useNavigation();
|
||||
const route = useRoute();
|
||||
// Hash of the exact seed the caller intends this account to load. Threaded in
|
||||
// from every entry point (create/restore via pin.js, relogin via pin/biometric,
|
||||
// no-security via App.tsx initialParams). We refuse to derive the account
|
||||
// identity until the in-context accountMnemoinc hashes to this value — see the
|
||||
// gate in the connect effect below.
|
||||
const expectedMnemonicHash = route.params?.expectedMnemonicHash;
|
||||
const {
|
||||
toggleMasterInfoObject,
|
||||
masterInfoObject,
|
||||
@@ -190,12 +202,47 @@ export default function ConnectingToNodeLoadingScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
if (preloadedUserData.isLoading && !preloadedUserData.data) return;
|
||||
if (didRunConnectionRef.current) return;
|
||||
if (preloadedUserData.isLoading && !preloadedUserData.data) return;
|
||||
|
||||
// ── Seed gate ─────────────────────────────────────────────────────────
|
||||
// The account identity (UUID + spark identity) is derived from
|
||||
// accountMnemoinc below. Under the restore/login navigation race this
|
||||
// effect can run while accountMnemoinc is still empty/stale — deriving the
|
||||
// identity from the wrong seed while the wallet connects with the correct
|
||||
// one (real funds, wrong account). Latch the derivation only once the
|
||||
// in-context seed is exactly the seed the caller intended. We do NOT set
|
||||
// didRunConnectionRef here, so the effect re-runs (accountMnemoinc is a dep)
|
||||
// and latches the instant the seed converges.
|
||||
if (!accountMnemoinc) return;
|
||||
if (expectedMnemonicHash) {
|
||||
if (sha256Hash(accountMnemoinc) !== expectedMnemonicHash) return;
|
||||
}
|
||||
|
||||
didRunConnectionRef.current = true;
|
||||
|
||||
requestAnimationFrame(startConnectProcess);
|
||||
}, [preloadedUserData, masterInfoObject]);
|
||||
}, [
|
||||
preloadedUserData,
|
||||
masterInfoObject,
|
||||
accountMnemoinc,
|
||||
expectedMnemonicHash,
|
||||
]);
|
||||
|
||||
// Safety valve for the seed gate: if the in-context seed never converges to
|
||||
// the expected one (should not happen once the eager restore-path generation
|
||||
// is fixed), surface the recoverable error UI instead of spinning forever.
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (!didRunConnectionRef.current) {
|
||||
setHasError({
|
||||
title: t('screens.inAccount.loadingScreen.initErrorTitle'),
|
||||
subtitle: t('screens.inAccount.loadingScreen.userSettingsError'),
|
||||
});
|
||||
}
|
||||
}, 30000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<GlobalThemeView useStandardWidth={true}>
|
||||
|
||||
@@ -44,10 +44,11 @@ export function LiquidEventProvider({ children }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!liquidNodeInformation.didConnectToNode) return;
|
||||
if (!accountMnemoinc) return;
|
||||
if (initialLiquidRun.current) return;
|
||||
initialLiquidRun.current = true;
|
||||
startLiquidEventListener(6);
|
||||
}, [liquidNodeInformation.didConnectToNode]);
|
||||
}, [liquidNodeInformation.didConnectToNode, accountMnemoinc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialRender.current) {
|
||||
|
||||
@@ -308,10 +308,11 @@ export const RootstockSwapProvider = ({ children }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!sparkInformation.identityPubKey) return;
|
||||
if (!accountMnemoinc) return;
|
||||
if (didRunSignerCreation.current) return;
|
||||
didRunSignerCreation.current = true;
|
||||
createSigner();
|
||||
}, [sparkInformation.identityPubKey]);
|
||||
}, [sparkInformation.identityPubKey, accountMnemoinc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!signer) return;
|
||||
|
||||
Reference in New Issue
Block a user