Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions app/src/providers/authProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ export async function getOrGeneratePointsAddress(
return pointsAddr;
}

export function getPrivateKeyFromMnemonic(mnemonic: string) {
const wallet = ethers.HDNodeWallet.fromPhrase(mnemonic);
return wallet.privateKey;
}

export async function hasSecretStored() {
const seed = await Keychain.getGenericPassword({ service: SERVICE_NAME });
return !!seed;
Expand Down Expand Up @@ -470,8 +475,7 @@ export async function unsafe_getPrivateKey(keychainOptions?: KeychainOptions) {
return null;
}
const mnemonic = JSON.parse(foundMnemonic) as Mnemonic;
const wallet = ethers.HDNodeWallet.fromPhrase(mnemonic.phrase);
return wallet.privateKey;
return getPrivateKeyFromMnemonic(mnemonic.phrase);
}

export const useAuth = () => {
Expand Down
70 changes: 48 additions & 22 deletions app/src/screens/account/recovery/AccountRecoveryChoiceScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ import RestoreAccountSvg from '@/assets/icons/restore_account.svg';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { RootStackParamList } from '@/navigation';
import { useAuth } from '@/providers/authProvider';
import { getPrivateKeyFromMnemonic, useAuth } from '@/providers/authProvider';
import {
loadPassportDataAndSecret,
loadPassportData,
reStorePassportDataWithRightCSCA,
} from '@/providers/passportDataProvider';
import { STORAGE_NAME, useBackupMnemonic } from '@/services/cloud-backup';
Expand Down Expand Up @@ -85,40 +85,66 @@ const AccountRecoveryChoiceScreen: React.FC = () => {
return false;
}

const passportDataAndSecret =
(await loadPassportDataAndSecret()) as string;
const { passportData, secret } = JSON.parse(passportDataAndSecret);
const passportData = await loadPassportData();
const secret = getPrivateKeyFromMnemonic(mnemonic.phrase);

if (!passportData || !secret) {
console.warn('Failed to load passport data or secret');
trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_AUTH, {
reason: 'no_passport_data_or_secret',
});
navigation.navigate({ name: 'Home', params: {} });
setRestoring(false);
return false;
}

const passportDataParsed = JSON.parse(passportData);

const { isRegistered, csca } =
await isUserRegisteredWithAlternativeCSCA(passportData, secret, {
getCommitmentTree(docCategory) {
return useProtocolStore.getState()[docCategory].commitment_tree;
},
getAltCSCA(docCategory) {
if (docCategory === 'aadhaar') {
const publicKeys =
useProtocolStore.getState().aadhaar.public_keys;
// Convert string[] to Record<string, string> format expected by AlternativeCSCA
return publicKeys
? Object.fromEntries(publicKeys.map(key => [key, key]))
: {};
}
await isUserRegisteredWithAlternativeCSCA(
passportDataParsed,
secret as string,
{
getCommitmentTree(docCategory) {
return useProtocolStore.getState()[docCategory].commitment_tree;
},
getAltCSCA(docCategory) {
if (docCategory === 'aadhaar') {
const publicKeys =
useProtocolStore.getState().aadhaar.public_keys;
// Convert string[] to Record<string, string> format expected by AlternativeCSCA
return publicKeys
? Object.fromEntries(publicKeys.map(key => [key, key]))
: {};
}

return useProtocolStore.getState()[docCategory].alternative_csca;
return useProtocolStore.getState()[docCategory]
.alternative_csca;
},
},
});
);
if (!isRegistered) {
console.warn(
'Secret provided did not match a registered ID. Please try again.',
);
trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_PASSPORT_NOT_REGISTERED);
trackEvent(
BackupEvents.CLOUD_RESTORE_FAILED_PASSPORT_NOT_REGISTERED,
{
reason: 'document_not_registered',
hasCSCA: !!csca,
},
);
navigation.navigate({ name: 'Home', params: {} });
setRestoring(false);
return false;
}
if (isCloudRestore && !cloudBackupEnabled) {
toggleCloudBackupEnabled();
}
reStorePassportDataWithRightCSCA(passportData, csca as string);
await reStorePassportDataWithRightCSCA(
passportDataParsed,
csca as string,
);
await markCurrentDocumentAsRegistered(selfClient);
trackEvent(BackupEvents.CLOUD_RESTORE_SUCCESS);
trackEvent(BackupEvents.ACCOUNT_RECOVERY_COMPLETED);
Expand Down
17 changes: 10 additions & 7 deletions app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ import {

import Paste from '@/assets/icons/paste.svg';
import type { RootStackParamList } from '@/navigation';
import { useAuth } from '@/providers/authProvider';
import { getPrivateKeyFromMnemonic, useAuth } from '@/providers/authProvider';
import {
loadPassportDataAndSecret,
loadPassportData,
reStorePassportDataWithRightCSCA,
} from '@/providers/passportDataProvider';

Expand Down Expand Up @@ -74,8 +74,10 @@ const RecoverWithPhraseScreen: React.FC = () => {
return;
}

const passportDataAndSecret = await loadPassportDataAndSecret();
if (!passportDataAndSecret) {
const passportData = await loadPassportData();
const secret = getPrivateKeyFromMnemonic(slimMnemonic);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Recovery derivation uses parameter instead of stored mnemonic

The new code derives the private secret directly from the user-provided mnemonic parameter instead of loading the stored mnemonic from keychain like the old code did. If any discrepancy exists between the parameter and what was actually stored by restoreAccountFromMnemonic (due to encoding, normalization, or storage issues), the derived secret will be incorrect and cause authentication to fail. The old approach validated consistency by loading from the authoritative stored source.

Fix in Cursor Fix in Web

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the goal here is to reduce the amount of mnemonic verifications that the user needs to perform

#1434


if (!passportData || !secret) {
console.warn(
'No passport data found on device. Please scan or import your document.',
);
Expand All @@ -86,9 +88,10 @@ const RecoverWithPhraseScreen: React.FC = () => {
setRestoring(false);
return;
}
const { passportData, secret } = JSON.parse(passportDataAndSecret);
const passportDataParsed = JSON.parse(passportData);

const { isRegistered, csca } = await isUserRegisteredWithAlternativeCSCA(
passportData,
passportDataParsed,
secret as string,
{
getCommitmentTree(docCategory) {
Expand Down Expand Up @@ -122,7 +125,7 @@ const RecoverWithPhraseScreen: React.FC = () => {
}

if (csca) {
await reStorePassportDataWithRightCSCA(passportData, csca);
await reStorePassportDataWithRightCSCA(passportDataParsed, csca);
}

await markCurrentDocumentAsRegistered(selfClient);
Expand Down
Loading