diff --git a/native/account/restore-backup-screen.react.js b/native/account/restore-backup-screen.react.js index 1fa9dc7b7..751181d65 100644 --- a/native/account/restore-backup-screen.react.js +++ b/native/account/restore-backup-screen.react.js @@ -1,83 +1,84 @@ // @flow import * as React from 'react'; import { Text, View } from 'react-native'; import * as Progress from 'react-native-progress'; import RegistrationContainer from './registration/registration-container.react.js'; import RegistrationContentContainer from './registration/registration-content-container.react.js'; import type { SignInNavigationProp } from './sign-in-navigator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useColors, useStyles } from '../themes/colors.js'; type Props = { +navigation: SignInNavigationProp<'RestoreBackupScreen'>, +route: NavigationRoute<'RestoreBackupScreen'>, }; export type RestoreBackupScreenParams = { - +username: string, + +userIdentifier: string, +credentials: | { +type: 'password', +password: string, } | { +type: 'siwe', + +secret: string, +message: string, +signature: string, }, }; // eslint-disable-next-line no-unused-vars function RestoreBackupScreen(props: Props): React.Node { const styles = useStyles(unboundStyles); const colors = useColors(); return ( Restoring from backup Your data is currently being restored. You will be automatically navigated to the app after this process is finished. ); } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, section: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 16, }, progressContainer: { flexGrow: 1, alignItems: 'center', justifyContent: 'center', }, scrollViewContentContainer: { flexGrow: 1, }, }; export default RestoreBackupScreen; diff --git a/native/account/restore-password-account-screen.react.js b/native/account/restore-password-account-screen.react.js index a5c96c327..6f5f04215 100644 --- a/native/account/restore-password-account-screen.react.js +++ b/native/account/restore-password-account-screen.react.js @@ -1,102 +1,102 @@ // @flow import * as React from 'react'; import { Text, TextInput, View } from 'react-native'; import PromptButton from './prompt-button.react.js'; import RegistrationButtonContainer from './registration/registration-button-container.react.js'; import RegistrationContainer from './registration/registration-container.react.js'; import RegistrationContentContainer from './registration/registration-content-container.react.js'; import RegistrationTextInput from './registration/registration-text-input.react.js'; import type { SignInNavigationProp } from './sign-in-navigator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { RestoreBackupScreenRouteName } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; type Props = { +navigation: SignInNavigationProp<'RestorePasswordAccountScreen'>, +route: NavigationRoute<'RestorePasswordAccountScreen'>, }; function RestorePasswordAccountScreen(props: Props): React.Node { const [username, setUsername] = React.useState(''); const [password, setPassword] = React.useState(''); const passwordInputRef = React.useRef>(); const focusPasswordInput = React.useCallback(() => { passwordInputRef.current?.focus(); }, []); const areCredentialsPresent = !!username && !!password; const onProceed = React.useCallback(() => { if (areCredentialsPresent) { props.navigation.navigate(RestoreBackupScreenRouteName, { - username, + userIdentifier: username, credentials: { type: 'password', password, }, }); } }, [areCredentialsPresent, password, props.navigation, username]); const styles = useStyles(unboundStyles); return ( Restore with password ); } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, buttonContainer: { flexDirection: 'row', }, password: { marginTop: 16, }, }; export default RestorePasswordAccountScreen; diff --git a/native/account/restore-prompt-screen.react.js b/native/account/restore-prompt-screen.react.js index 65d9a1aba..f45c4199f 100644 --- a/native/account/restore-prompt-screen.react.js +++ b/native/account/restore-prompt-screen.react.js @@ -1,125 +1,177 @@ // @flow import * as React from 'react'; import { Text, View } from 'react-native'; +import type { SIWEResult } from 'lib/types/siwe-types.js'; +import { getMessageForException } from 'lib/utils/errors.js'; + import PromptButton from './prompt-button.react.js'; import RegistrationButtonContainer from './registration/registration-button-container.react.js'; import RegistrationContainer from './registration/registration-container.react.js'; import RegistrationContentContainer from './registration/registration-content-container.react.js'; import type { SignInNavigationProp } from './sign-in-navigator.react'; import { useSIWEPanelState } from './siwe-hooks.js'; import SIWEPanel from './siwe-panel.react.js'; +import { useClientBackup } from '../backup/use-client-backup.js'; import type { NavigationRoute } from '../navigation/route-names'; -import { RestorePasswordAccountScreenRouteName } from '../navigation/route-names.js'; +import { + RestoreSIWEBackupRouteName, + RestorePasswordAccountScreenRouteName, +} from '../navigation/route-names.js'; import { useColors, useStyles } from '../themes/colors.js'; +import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; +import Alert from '../utils/alert.js'; import RestoreIcon from '../vectors/restore-icon.react.js'; type Props = { +navigation: SignInNavigationProp<'RestorePromptScreen'>, +route: NavigationRoute<'RestorePromptScreen'>, }; const siweSignatureRequestData = { messageType: 'msg_auth', }; function RestorePromptScreen(props: Props): React.Node { const styles = useStyles(unboundStyles); const openPasswordRestoreScreen = React.useCallback(() => { props.navigation.navigate(RestorePasswordAccountScreenRouteName); }, [props.navigation]); + const { retrieveLatestBackupInfo } = useClientBackup(); + const onSIWESuccess = React.useCallback( + async (result: SIWEResult) => { + try { + const { address, signature, message } = result; + const backupInfo = await retrieveLatestBackupInfo(address); + const { siweBackupData } = backupInfo; + + if (!siweBackupData) { + throw new Error('Missing SIWE message for Wallet user backup'); + } + + const { + siweBackupMsgNonce, + siweBackupMsgIssuedAt, + siweBackupMsgStatement, + } = siweBackupData; + + props.navigation.navigate(RestoreSIWEBackupRouteName, { + siweNonce: siweBackupMsgNonce, + siweStatement: siweBackupMsgStatement, + siweIssuedAt: siweBackupMsgIssuedAt, + userIdentifier: address, + signature, + message, + }); + } catch (e) { + const messageForException = getMessageForException(e); + console.log( + `SIWE restore error: ${messageForException ?? 'unknown error'}`, + ); + const alertDetails = unknownErrorAlertDetails; + Alert.alert( + alertDetails.title, + alertDetails.message, + [{ text: 'OK', onPress: props.navigation.goBack }], + { cancelable: false }, + ); + } + }, + [props.navigation, retrieveLatestBackupInfo], + ); + const { panelState, openPanel, onPanelClosed, onPanelClosing, siwePanelSetLoading, } = useSIWEPanelState(); let siwePanel; if (panelState !== 'closed') { siwePanel = ( {}} + onSuccessfulWalletSignature={onSIWESuccess} siweSignatureRequestData={siweSignatureRequestData} setLoading={siwePanelSetLoading} /> ); } const colors = useColors(); return ( <> Restore account If you’ve lost access to your primary device, you can try recovering your Comm account. To proceed, select the same login method that you used during registration. Note that after completing the recovery flow, you will be logged out from all of your other devices. {siwePanel} ); } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, section: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 16, }, iconContainer: { flexGrow: 1, alignItems: 'center', justifyContent: 'center', }, buttonContainer: { flexDirection: 'row', }, scrollViewContentContainer: { flexGrow: 1, }, }; export default RestorePromptScreen; diff --git a/native/backup/restore-siwe-backup.react.js b/native/backup/restore-siwe-backup.react.js index 25f02440c..60c952ad6 100644 --- a/native/backup/restore-siwe-backup.react.js +++ b/native/backup/restore-siwe-backup.react.js @@ -1,85 +1,68 @@ // @flow import * as React from 'react'; -import { Alert } from 'react-native'; import { type SIWEResult } from 'lib/types/siwe-types.js'; -import { getMessageForException } from 'lib/utils/errors.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 { + type NavigationRoute, + RestoreBackupScreenRouteName, +} from '../navigation/route-names.js'; export type RestoreSIWEBackupParams = { - +backupID: string, +siweNonce: string, +siweStatement: string, +siweIssuedAt: string, +userIdentifier: string, + +signature: string, + +message: string, }; type Props = { +navigation: RootNavigationProp<'RestoreSIWEBackup'>, +route: NavigationRoute<'RestoreSIWEBackup'>, }; function RestoreSIWEBackup(props: Props): React.Node { const { goBack } = props.navigation; const { route } = props; const { params: { - backupID, siweStatement, siweIssuedAt, siweNonce, userIdentifier, + signature, + message, }, } = route; - const { getBackupUserKeys } = useClientBackup(); - const onSuccessfulWalletSignature = React.useCallback( (result: SIWEResult) => { - void (async () => { - const { signature } = result; - let message = 'success'; - try { - const { backupDataKey, backupLogDataKey } = await getBackupUserKeys( - userIdentifier, - signature, - backupID, - ); - await commCoreModule.restoreBackupData( - backupID, - backupDataKey, - backupLogDataKey, - persistConfig.version.toString(), - ); - } catch (e) { - message = `Backup restore error: ${String( - getMessageForException(e), - )}`; - console.error(message); - } - Alert.alert('Restore protocol result', message); - goBack(); - })(); + props.navigation.navigate(RestoreBackupScreenRouteName, { + userIdentifier, + credentials: { + type: 'siwe', + secret: result.signature, + message, + signature, + }, + }); }, - [backupID, getBackupUserKeys, goBack, userIdentifier], + [message, props.navigation, signature, userIdentifier], ); return ( ); } export default RestoreSIWEBackup; diff --git a/native/profile/backup-menu.react.js b/native/profile/backup-menu.react.js index f796e05f6..af42c52c0 100644 --- a/native/profile/backup-menu.react.js +++ b/native/profile/backup-menu.react.js @@ -1,393 +1,310 @@ // @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 { 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 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 identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); const { identityClient, getAuthMetadata } = identityContext; const userIdentifier = useSelector(state => state.currentUserInfo?.username); invariant(userIdentifier, 'userIdentifier should be set'); 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 [{ backupID }, backupSecret] = await Promise.all([ - retrieveLatestBackupInfo(userIdentifier), - getBackupSecret(), - ]); - const { backupDataKey, backupLogDataKey } = await getBackupUserKeys( - userIdentifier, - backupSecret, - backupID, - ); - await commCoreModule.restoreBackupData( - backupID, - 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, - getBackupUserKeys, - retrieveLatestBackupInfo, - userIdentifier, - ]); - const testLatestBackupInfo = React.useCallback(async () => { let message; try { const { backupID, userID } = await retrieveLatestBackupInfo(userIdentifier); 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, userIdentifier, 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 { userID, backupID } = await retrieveLatestBackupInfo(userIdentifier); 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); }, [ retrieveLatestBackupInfo, userIdentifier, currentUserInfo?.id, identityClient, getAuthMetadata, getBackupSecret, getBackupUserKeys, ]); - const testRestoreForSIWEUser = React.useCallback(async () => { - let message = 'success'; - try { - const { siweBackupData, backupID } = - await retrieveLatestBackupInfo(userIdentifier); - - 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); - } - }, [retrieveLatestBackupInfo, userIdentifier, navigation]); - 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;