diff --git a/native/backup/backup-handler.js b/native/backup/backup-handler.js --- a/native/backup/backup-handler.js +++ b/native/backup/backup-handler.js @@ -1,13 +1,28 @@ // @flow +import invariant from 'invariant'; import * as React from 'react'; +import { setPeerDeviceListsActionType } from 'lib/actions/aux-user-actions.js'; import { createUserKeysBackupActionTypes } from 'lib/actions/backup-actions.js'; +import { useBroadcastDeviceListUpdates } from 'lib/hooks/peer-list-hooks.js'; import { useCheckIfPrimaryDevice } from 'lib/hooks/primary-device-hooks.js'; -import { isLoggedIn } from 'lib/selectors/user-selectors.js'; +import { isLoggedIn, getAllPeerDevices } from 'lib/selectors/user-selectors.js'; +import { signDeviceListUpdate } from 'lib/shared/device-list-utils.js'; +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useStaffAlert } from 'lib/shared/staff-utils.js'; +import type { + RawDeviceList, + SignedDeviceList, +} from 'lib/types/identity-service-types.js'; +import { + composeRawDeviceList, + rawDeviceListFromSignedList, +} from 'lib/utils/device-list-utils.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; +import { useDispatch } from 'lib/utils/redux-utils.js'; +import { usingRestoreFlow } from 'lib/utils/services-utils.js'; import { useClientBackup } from './use-client-backup.js'; import { useGetBackupSecretForLoggedInUser } from './use-get-backup-secret.js'; @@ -17,6 +32,30 @@ const millisecondsPerDay = 24 * 60 * 60 * 1000; +async function reorderAndSignDeviceList( + thisDeviceID: string, + currentDeviceList: RawDeviceList, +): Promise<{ + +rawList: RawDeviceList, + +signedList: SignedDeviceList, +}> { + const currentDevices = [...currentDeviceList.devices]; + + const thisDeviceIndex = currentDevices.indexOf(thisDeviceID); + if (thisDeviceIndex < 0) { + throw new Error("Device list doesn't contain current device ID"); + } + + const newDevices = + thisDeviceIndex === 0 + ? currentDevices + : [thisDeviceID, ...currentDevices.splice(thisDeviceIndex, 1)]; + + const rawList = composeRawDeviceList(newDevices); + const signedList = await signDeviceListUpdate(rawList); + return { rawList, signedList }; +} + function BackupHandler(): null { const loggedIn = useSelector(isLoggedIn); const staffCanSee = useStaffCanSee(); @@ -37,6 +76,15 @@ const backupUploadInProgress = React.useRef(false); const [handlerStarted, setHandlerStarted] = React.useState(false); + const identityContext = React.useContext(IdentityClientContext); + invariant(identityContext, 'Identity context should be set'); + + const migrationRunning = React.useRef(false); + const dispatch = useDispatch(); + + const broadcastDeviceListUpdates = useBroadcastDeviceListUpdates(); + const allPeerDevices = useSelector(getAllPeerDevices); + React.useEffect(() => { if (!staffCanSee) { return; @@ -141,13 +189,34 @@ void (async () => { backupUploadInProgress.current = true; + + const { getAuthMetadata, identityClient } = identityContext; + const { userID, deviceID } = await getAuthMetadata(); + if (!userID) { + throw new Error('Missing auth metadata'); + } + if (!userIdentifier) { + throw new Error('Missing userIdentifier'); + } + const deviceListsResponse = await identityClient.getDeviceListsForUsers([ + userID, + ]); + const currentDeviceList = + deviceListsResponse.usersSignedDeviceLists[userID]; + const currentUserPlatformDetails = + deviceListsResponse.usersDevicesPlatformDetails[userID]; + if (!currentDeviceList || !currentUserPlatformDetails) { + throw new Error('Device list not found for current user'); + } + + const deviceListIsSigned = !!currentDeviceList.curPrimarySignature; const isPrimaryDevice = await checkIfPrimaryDevice(); - if (!isPrimaryDevice) { + if (!isPrimaryDevice && deviceListIsSigned) { backupUploadInProgress.current = false; return; } - if (latestBackupInfo) { + if (isPrimaryDevice && latestBackupInfo) { const timestamp = latestBackupInfo.timestamp; if (timestamp >= Date.now() - millisecondsPerDay) { backupUploadInProgress.current = false; @@ -165,11 +234,75 @@ try { const promise = (async () => { - const backupID = await createUserKeysBackup(); - return { - backupID, - timestamp: Date.now(), - }; + if (usingRestoreFlow && !latestBackupInfo && !deviceListIsSigned) { + try { + migrationRunning.current = true; + if (!userID || !deviceID) { + throw new Error('Missing auth metadata'); + } + + const { updateDeviceList } = identityClient; + invariant( + updateDeviceList, + 'updateDeviceList() should be defined on native. ' + + 'Are you calling it on a non-primary device?', + ); + + // 1. upload UserKeys (without updating the store) + const backupID = await createUserKeysBackup(); + + // 2. create in-memory device list (reorder and sign) + const newDeviceList = await reorderAndSignDeviceList( + deviceID, + rawDeviceListFromSignedList(currentDeviceList), + ); + + // 3. UpdateDeviceList RPC transaction + await updateDeviceList(newDeviceList.signedList); + + // 4. fetch backupID again and compare + const fetchedBackupInfo = + await retrieveLatestBackupInfo(userIdentifier); + if (!fetchedBackupInfo.backupID !== backupID) { + // TODO: - if not equal, upload backup keys again + throw new Error('BackupID doesnt match'); + } + + // 5a. Update self device list + if (!userID) { + // flow - says userID can be void even despite above check + throw new Error('Missing auth metadata'); + } + dispatch({ + type: setPeerDeviceListsActionType, + payload: { + deviceLists: { [userID]: newDeviceList.rawList }, + usersPlatformDetails: { + [userID]: currentUserPlatformDetails, + }, + }, + }); + // 5b. Broadcast update to peers + void broadcastDeviceListUpdates( + allPeerDevices.filter(id => id !== deviceID), + ); + + // 6. Set store value (dispatchActionPromise success return value) + return { + backupID, + timestamp: Date.now(), + }; + } finally { + migrationRunning.current = false; + backupUploadInProgress.current = false; + } + } else { + const backupID = await createUserKeysBackup(); + return { + backupID, + timestamp: Date.now(), + }; + } })(); void dispatchActionPromise(createUserKeysBackupActionTypes, promise); await promise; @@ -181,15 +314,21 @@ backupUploadInProgress.current = false; })(); }, [ + allPeerDevices, + broadcastDeviceListUpdates, canPerformBackupOperation, checkIfPrimaryDevice, createUserKeysBackup, + dispatch, dispatchActionPromise, handlerStarted, + identityContext, latestBackupInfo, + retrieveLatestBackupInfo, showAlertToStaff, staffCanSee, testUserKeysRestore, + userIdentifier, ]); return null;