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;