diff --git a/native/backup/restore-siwe-backup.react.js b/native/backup/restore-siwe-backup.react.js index 427387f76..ab4c952a1 100644 --- a/native/backup/restore-siwe-backup.react.js +++ b/native/backup/restore-siwe-backup.react.js @@ -1,102 +1,99 @@ // @flow import * as React from 'react'; 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'; import { type NavigationRoute } from '../navigation/route-names.js'; import { persistConfig } from '../redux/persist.js'; import { useStyles } from '../themes/colors.js'; export type RestoreSIWEBackupParams = { +backupID: string, +siweNonce: string, +siweStatement: string, +siweIssuedAt: string, +userIdentifier: string, }; type Props = { +navigation: RootNavigationProp<'RestoreSIWEBackup'>, +route: NavigationRoute<'RestoreSIWEBackup'>, }; function RestoreSIWEBackup(props: Props): React.Node { const styles = useStyles(unboundStyles); const { goBack } = props.navigation; const { route } = props; const { params: { backupID, siweStatement, siweIssuedAt, siweNonce, userIdentifier, }, } = route; + const { getBackupUserKeys } = useClientBackup(); + const onSuccessfulWalletSignature = React.useCallback( (result: SIWEResult) => { void (async () => { const { signature } = result; let message = 'success'; try { - const userKeysResponse = await commCoreModule.getBackupUserKeys( + const { backupDataKey, backupLogDataKey } = await getBackupUserKeys( userIdentifier, signature, backupID, ); - const userKeys = assertWithValidator( - JSON.parse(userKeysResponse), - userKeysResponseValidator, - ); await commCoreModule.restoreBackupData( backupID, - userKeys.backupDataKey, - userKeys.backupLogDataKey, + backupDataKey, + backupLogDataKey, persistConfig.version.toString(), ); } catch (e) { message = `Backup restore error: ${String( getMessageForException(e), )}`; console.error(message); } Alert.alert('Restore protocol result', message); goBack(); })(); }, - [backupID, goBack, userIdentifier], + [backupID, getBackupUserKeys, goBack, userIdentifier], ); return ( ); } const safeAreaEdges = ['top']; const unboundStyles = { container: { flex: 1, backgroundColor: 'panelBackground', justifyContent: 'space-between', }, }; export default RestoreSIWEBackup; diff --git a/native/backup/use-client-backup.js b/native/backup/use-client-backup.js index 55de03b3f..320e38b51 100644 --- a/native/backup/use-client-backup.js +++ b/native/backup/use-client-backup.js @@ -1,77 +1,101 @@ // @flow import * as React from 'react'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { latestBackupInfoResponseValidator, type LatestBackupInfo, + type UserKeys, + userKeysResponseValidator, } from 'lib/types/backup-types.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; import { useGetBackupSecretForLoggedInUser } from './use-get-backup-secret.js'; import { commCoreModule } from '../native-modules.js'; import { useSelector } from '../redux/redux-utils.js'; type ClientBackup = { +createFullBackup: () => Promise, +createUserKeysBackup: () => Promise, +retrieveLatestBackupInfo: () => Promise<{ +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, ); const currentUserInfo = useSelector(state => state.currentUserInfo); const loggedIn = useSelector(isLoggedIn); const getBackupSecret = useGetBackupSecretForLoggedInUser(); const createFullBackup = React.useCallback(async () => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to upload backup for not logged in user.'); } const backupSecret = await getBackupSecret(); return commCoreModule.createFullBackup(backupSecret); }, [loggedIn, currentUserID, getBackupSecret]); const createUserKeysBackup = React.useCallback(async () => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to upload User Keys for not logged in user.'); } const backupSecret = await getBackupSecret(); return commCoreModule.createUserKeysBackup(backupSecret); }, [loggedIn, currentUserID, getBackupSecret]); const retrieveLatestBackupInfo = React.useCallback(async () => { if (!loggedIn || !currentUserID || !currentUserInfo?.username) { throw new Error('Attempt to restore backup for not logged in user.'); } const userIdentifier = currentUserInfo?.username; const response = await commCoreModule.retrieveLatestBackupInfo(userIdentifier); const latestBackupInfo = assertWithValidator( JSON.parse(response), latestBackupInfoResponseValidator, ); return { latestBackupInfo, userIdentifier }; }, [currentUserID, currentUserInfo, loggedIn]); return React.useMemo( () => ({ createFullBackup, createUserKeysBackup, retrieveLatestBackupInfo, + getBackupUserKeys, }), - [retrieveLatestBackupInfo, createFullBackup, createUserKeysBackup], + [createFullBackup, createUserKeysBackup, retrieveLatestBackupInfo], ); } export { useClientBackup }; diff --git a/native/profile/backup-menu.react.js b/native/profile/backup-menu.react.js index 3e339e410..69802097d 100644 --- a/native/profile/backup-menu.react.js +++ b/native/profile/backup-menu.react.js @@ -1,270 +1,385 @@ // @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'; import { useGetBackupSecretForLoggedInUser } from '../backup/use-get-backup-secret.js'; import Button from '../components/button.react.js'; import { commCoreModule } from '../native-modules.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { RestoreSIWEBackupRouteName } from '../navigation/route-names.js'; import { setLocalSettingsActionType } from '../redux/action-types.js'; import { persistConfig } from '../redux/persist.js'; import { useSelector } from '../redux/redux-utils.js'; import { useColors, useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; type Props = { +navigation: ProfileNavigationProp<'BackupMenu'>, +route: NavigationRoute<'BackupMenu'>, }; // eslint-disable-next-line no-unused-vars function BackupMenu(props: Props): React.Node { const styles = useStyles(unboundStyles); const dispatch = useDispatch(); const colors = useColors(); const currentUserInfo = useSelector(state => state.currentUserInfo); const navigation = useNavigation(); const getBackupSecret = useGetBackupSecretForLoggedInUser(); const isBackupEnabled = useSelector( 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; try { const backupID = await createFullBackup(); message = `Success!\n` + `Backup ID: ${backupID}`; } catch (e) { message = `Backup upload error: ${String(getMessageForException(e))}`; console.error(message); } Alert.alert('Upload protocol result', message); }, [createFullBackup]); const uploadUserKeys = React.useCallback(async () => { let message; try { const backupID = await createUserKeysBackup(); message = `Success!\n` + `Backup ID: ${backupID}`; } catch (e) { message = `User Keys upload error: ${String(getMessageForException(e))}`; console.error(message); } Alert.alert('Upload User Keys result', message); }, [createUserKeysBackup]); const testRestoreForPasswordUser = React.useCallback(async () => { let message = 'success'; 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.'); } catch (e) { message = `Backup restore error: ${String(getMessageForException(e))}`; console.error(message); } Alert.alert('Restore protocol result', message); - }, [getBackupSecret, retrieveLatestBackupInfo]); + }, [getBackupSecret, getBackupUserKeys, retrieveLatestBackupInfo]); const testLatestBackupInfo = React.useCallback(async () => { let message; try { const { latestBackupInfo } = await retrieveLatestBackupInfo(); const { backupID, userID } = latestBackupInfo; message = `Success!\n` + `Backup ID: ${backupID},\n` + `userID: ${userID},\n` + `userID check: ${currentUserInfo?.id === userID ? 'true' : 'false'}`; } catch (e) { message = `Latest backup info error: ${String( getMessageForException(e), )}`; console.error(message); } 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 = + `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('Signing with previous primary Olm Account result', message); + }, [ + currentUserInfo?.id, + getAuthMetadata, + getBackupSecret, + getBackupUserKeys, + identityClient, + retrieveLatestBackupInfo, + ]); + const testRestoreForSIWEUser = React.useCallback(async () => { let message = 'success'; try { const { latestBackupInfo, userIdentifier } = await retrieveLatestBackupInfo(); const { siweBackupData, backupID } = latestBackupInfo; if (!siweBackupData) { throw new Error('Missing SIWE message for Wallet user backup'); } const { siweBackupMsgNonce, siweBackupMsgIssuedAt, siweBackupMsgStatement, } = siweBackupData; navigation.navigate<'RestoreSIWEBackup'>({ name: RestoreSIWEBackupRouteName, params: { backupID, siweNonce: siweBackupMsgNonce, siweStatement: siweBackupMsgStatement, siweIssuedAt: siweBackupMsgIssuedAt, userIdentifier, }, }); } catch (e) { message = `Backup restore error: ${String(getMessageForException(e))}`; console.error(message); } }, [navigation, retrieveLatestBackupInfo]); const onBackupToggled = React.useCallback( (value: boolean) => { dispatch({ type: setLocalSettingsActionType, payload: { isBackupEnabled: value }, }); }, [dispatch], ); const onPressRestoreButton = accountHasPassword(currentUserInfo) ? testRestoreForPasswordUser : testRestoreForSIWEUser; return ( SETTINGS Toggle automatic backup ACTIONS + + + ); } const unboundStyles = { scrollViewContentContainer: { paddingTop: 24, }, scrollView: { backgroundColor: 'panelBackground', }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, marginVertical: 2, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, submenuButton: { flexDirection: 'row', paddingHorizontal: 24, paddingVertical: 10, alignItems: 'center', }, submenuText: { color: 'panelForegroundLabel', flex: 1, fontSize: 16, }, row: { flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 14, }, }; export default BackupMenu;