diff --git a/native/backup/restore-siwe-backup.react.js b/native/backup/restore-siwe-backup.react.js --- a/native/backup/restore-siwe-backup.react.js +++ b/native/backup/restore-siwe-backup.react.js @@ -4,11 +4,10 @@ import { Alert } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { userKeysResponseValidator } from 'lib/types/backup-types.js'; import { type SIWEResult } from 'lib/types/siwe-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; -import { assertWithValidator } from 'lib/utils/validation-utils.js'; +import { useClientBackup } from './use-client-backup.js'; import { SignSIWEBackupMessageForRestore } from '../account/registration/siwe-backup-message-creation.react.js'; import { commCoreModule } from '../native-modules.js'; import { type RootNavigationProp } from '../navigation/root-navigator.react.js'; @@ -43,25 +42,23 @@ }, } = route; + const { getBackupUserKeys } = useClientBackup(); + const onSuccessfulWalletSignature = React.useCallback( (result: SIWEResult) => { void (async () => { const { signature } = result; let message = 'success'; try { - const userKeysResponse = commCoreModule.getBackupUserKeys( + const { backupDataKey, backupLogDataKey } = await getBackupUserKeys( userIdentifier, signature, backupID, ); - const userKeys = assertWithValidator( - userKeysResponse, - userKeysResponseValidator, - ); await commCoreModule.restoreBackupData( backupID, - userKeys.backupDataKey, - userKeys.backupLogDataKey, + backupDataKey, + backupLogDataKey, persistConfig.version.toString(), ); } catch (e) { @@ -74,7 +71,7 @@ goBack(); })(); }, - [backupID, goBack, userIdentifier], + [backupID, getBackupUserKeys, goBack, userIdentifier], ); return ( diff --git a/native/backup/use-client-backup.js b/native/backup/use-client-backup.js --- a/native/backup/use-client-backup.js +++ b/native/backup/use-client-backup.js @@ -6,6 +6,8 @@ import { latestBackupInfoResponseValidator, type LatestBackupInfo, + type UserKeys, + userKeysResponseValidator, } from 'lib/types/backup-types.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; @@ -20,8 +22,29 @@ +latestBackupInfo: LatestBackupInfo, +userIdentifier: string, }>, + +getBackupUserKeys: ( + userIdentifier: string, + backupSecret: string, + backupID: string, + ) => Promise, }; +async function getBackupUserKeys( + userIdentifier: string, + backupSecret: string, + backupID: string, +): Promise { + const userKeysResponse = await commCoreModule.getBackupUserKeys( + userIdentifier, + backupSecret, + backupID, + ); + return assertWithValidator( + JSON.parse(userKeysResponse), + userKeysResponseValidator, + ); +} + function useClientBackup(): ClientBackup { const currentUserID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, @@ -69,8 +92,9 @@ createFullBackup, createUserKeysBackup, retrieveLatestBackupInfo, + getBackupUserKeys, }), - [retrieveLatestBackupInfo, createFullBackup, createUserKeysBackup], + [createFullBackup, createUserKeysBackup, retrieveLatestBackupInfo], ); } diff --git a/native/profile/backup-menu.react.js b/native/profile/backup-menu.react.js --- a/native/profile/backup-menu.react.js +++ b/native/profile/backup-menu.react.js @@ -1,15 +1,17 @@ // @flow import { useNavigation } from '@react-navigation/native'; +import invariant from 'invariant'; import * as React from 'react'; import { Switch, Text, View } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { accountHasPassword } from 'lib/shared/account-utils.js'; -import { userKeysResponseValidator } from 'lib/types/backup-types.js'; +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; +import { getConfig } from 'lib/utils/config.js'; +import { rawDeviceListFromSignedList } from 'lib/utils/device-list-utils.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; -import { assertWithValidator } from 'lib/utils/validation-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import { useClientBackup } from '../backup/use-client-backup.js'; @@ -41,8 +43,16 @@ state => state.localSettings.isBackupEnabled, ); - const { createFullBackup, retrieveLatestBackupInfo, createUserKeysBackup } = - useClientBackup(); + const identityContext = React.useContext(IdentityClientContext); + invariant(identityContext, 'Identity context should be set'); + const { identityClient, getAuthMetadata } = identityContext; + + const { + createFullBackup, + retrieveLatestBackupInfo, + createUserKeysBackup, + getBackupUserKeys, + } = useClientBackup(); const uploadBackup = React.useCallback(async () => { let message; @@ -73,19 +83,15 @@ try { const [{ latestBackupInfo, userIdentifier }, backupSecret] = await Promise.all([retrieveLatestBackupInfo(), getBackupSecret()]); - const userKeysResponse = await commCoreModule.getBackupUserKeys( + const { backupDataKey, backupLogDataKey } = await getBackupUserKeys( userIdentifier, backupSecret, latestBackupInfo.backupID, ); - const userKeys = assertWithValidator( - JSON.parse(userKeysResponse), - userKeysResponseValidator, - ); await commCoreModule.restoreBackupData( latestBackupInfo.backupID, - userKeys.backupDataKey, - userKeys.backupLogDataKey, + backupDataKey, + backupLogDataKey, persistConfig.version.toString(), ); console.info('Backup restored.'); @@ -94,7 +100,7 @@ console.error(message); } Alert.alert('Restore protocol result', message); - }, [getBackupSecret, retrieveLatestBackupInfo]); + }, [getBackupSecret, getBackupUserKeys, retrieveLatestBackupInfo]); const testLatestBackupInfo = React.useCallback(async () => { let message; @@ -115,6 +121,103 @@ Alert.alert('Latest backup info result', message); }, [currentUserInfo?.id, retrieveLatestBackupInfo]); + const testSigning = React.useCallback(async () => { + // This test only works in the following case: + // 1. Logged in on Primary Device using v1 + // 2. Creating User Keys Backup on Primary + // 3. Log Out on Primary Device using v1 + // 4. Log In on any native device using v1 + // 5. Perform this test + let message; + try { + const { + latestBackupInfo: { userID, backupID }, + userIdentifier, + } = await retrieveLatestBackupInfo(); + + if (currentUserInfo?.id !== userID) { + throw new Error('Backup returned different userID'); + } + + // We fetch Device List history to get previous primary `deviceID` + const deviceLists = + await identityClient.getDeviceListHistoryForUser(userID); + if (deviceLists.length < 3) { + throw new Error( + 'Previous Primary Device issue: device list history too short', + ); + } + + // According to steps listed above, device list history looks like this: + // 1. [...], [lastPrimaryDeviceID] + // 2. [...], [lastPrimaryDeviceID] + // 3. [...], [lastPrimaryDeviceID], [] + // 4. [...], [lastPrimaryDeviceID], [], [currentPrimaryDeviceID] + // 5. [...], [lastPrimaryDeviceID], [], [currentPrimaryDeviceID] + // In order to get lastPrimaryDeviceID, we need to get the last + // but two item + const lastDeviceListWithPrimary = deviceLists[deviceLists.length - 3]; + const lastRawDeviceListWithPrimary = rawDeviceListFromSignedList( + lastDeviceListWithPrimary, + ); + const lastPrimaryDeviceID = lastRawDeviceListWithPrimary.devices[0]; + if (!lastPrimaryDeviceID) { + throw new Error('Previous Primary Device issue: empty device list'); + } + + const { deviceID } = await getAuthMetadata(); + if (deviceID === lastPrimaryDeviceID) { + throw new Error('Previous Primary Device issue: the same deviceIDs'); + } + + const backupSecret = await getBackupSecret(); + const { pickledAccount, pickleKey } = await getBackupUserKeys( + userIdentifier, + backupSecret, + backupID, + ); + + const emptyDeviceListMessage = '[]'; + + // Sign using Olm Account from backup + const signature = await commCoreModule.signMessageUsingAccount( + emptyDeviceListMessage, + pickledAccount, + pickleKey, + ); + + // Verify using previous primary `deviceID` + const { olmAPI } = getConfig(); + const verificationResult = await olmAPI.verifyMessage( + emptyDeviceListMessage, + signature, + lastPrimaryDeviceID, + ); + + message = + `Result:\n` + + `Backup ID: ${backupID},\n` + + `userID: ${userID},\n` + + `deviceID: ${deviceID},\n` + + `lastPrimaryDeviceID: ${lastPrimaryDeviceID},\n` + + `signature: ${signature},\n` + + `verificationResult: ${verificationResult.toString()}\n`; + } catch (e) { + message = `Latest backup info error: ${String( + getMessageForException(e), + )}`; + console.error(message); + } + Alert.alert('Latest backup info result', message); + }, [ + currentUserInfo?.id, + getAuthMetadata, + getBackupSecret, + getBackupUserKeys, + identityClient, + retrieveLatestBackupInfo, + ]); + const testRestoreForSIWEUser = React.useCallback(async () => { let message = 'success'; try { @@ -222,6 +325,19 @@ + + + ); }