diff --git a/lib/facts/backup-service.js b/lib/facts/backup-service.js deleted file mode 100644 index 0137c4e07..000000000 --- a/lib/facts/backup-service.js +++ /dev/null @@ -1,52 +0,0 @@ -// @flow - -import { isDev } from '../utils/dev-utils.js'; - -type BackupServiceEndpointPath = - | '/backups' - | '/backups/:backupID/user_keys' - | '/backups/:backupID/user_data' - | '/backups/latest/:username/backup_id' - | '/backups/latest/:username/user_keys'; - -export type BackupServiceHTTPEndpoint = { - +path: BackupServiceEndpointPath, - +method: 'GET' | 'POST', -}; - -const httpEndpoints = Object.freeze({ - // endpoints with auth - UPLOAD_BACKUP: { - path: '/backups', - method: 'POST', - }, - GET_USER_KEYS: { - path: '/backups/:backupID/user_keys', - method: 'GET', - }, - GET_USER_DATA: { - path: '/backups/:backupID/user_data', - method: 'GET', - }, - // endpoints without auth - GET_LATEST_BACKUP_ID: { - path: '/backups/latest/:username/backup_id', - method: 'GET', - }, - GET_LATEST_USER_KEYS: { - path: '/backups/latest/:username/user_keys', - method: 'GET', - }, -}); - -const config: { - url: string, - httpEndpoints: { +[endpoint: string]: BackupServiceHTTPEndpoint }, -} = { - url: isDev - ? 'https://backup.staging.commtechnologies.org' - : 'https://backup.commtechnologies.org', - httpEndpoints, -}; - -export default config; diff --git a/lib/types/backup-types.js b/lib/types/backup-types.js deleted file mode 100644 index 60bf32959..000000000 --- a/lib/types/backup-types.js +++ /dev/null @@ -1,30 +0,0 @@ -// @flow - -import type { UserStore } from './user-types.js'; - -export type UserKeys = { - +backupDataKey: string, - +ed25519: string, -}; - -export type UserData = { - +userStore: UserStore, -}; - -export type BackupAuth = { - +userID: string, - +accessToken: string, - +deviceID: string, -}; - -export type Backup = { - +backupID: string, - +userKeys: UserKeys, - +userData: UserData, -}; - -export type BackupEncrypted = { - +backupID: string, - +userKeys: string, - +userData: string, -}; diff --git a/lib/utils/backup-service.js b/lib/utils/backup-service.js deleted file mode 100644 index fde62f20b..000000000 --- a/lib/utils/backup-service.js +++ /dev/null @@ -1,15 +0,0 @@ -// @flow - -import { replacePathParams, type URLPathParams } from './url-utils.js'; -import backupServiceConfig from '../facts/backup-service.js'; -import type { BackupServiceHTTPEndpoint } from '../facts/backup-service.js'; - -function makeBackupServiceEndpointURL( - endpoint: BackupServiceHTTPEndpoint, - params: URLPathParams = {}, -): string { - const path = replacePathParams(endpoint.path, params); - return `${backupServiceConfig.url}${path}`; -} - -export { makeBackupServiceEndpointURL }; diff --git a/native/backup/api.js b/native/backup/api.js deleted file mode 100644 index f203619db..000000000 --- a/native/backup/api.js +++ /dev/null @@ -1,119 +0,0 @@ -// @flow - -import base64 from 'base-64'; - -import backupService from 'lib/facts/backup-service.js'; -import type { BackupAuth, BackupEncrypted } from 'lib/types/backup-types.js'; -import { makeBackupServiceEndpointURL } from 'lib/utils/backup-service.js'; -import { toBase64URL } from 'lib/utils/base64.js'; -import { handleHTTPResponseError } from 'lib/utils/services-utils.js'; - -import { getBackupStringFromBlob } from './conversion-utils.js'; -import { commUtilsModule } from '../native-modules.js'; - -function getBackupFormData(backup: BackupEncrypted): FormData { - const { backupID, userKeys, userData } = backup; - const userKeysHash = commUtilsModule.sha256( - commUtilsModule.encodeStringToUTF8ArrayBuffer(userKeys), - ); - const userDataHash = commUtilsModule.sha256( - commUtilsModule.encodeStringToUTF8ArrayBuffer(userData), - ); - - const formData = new FormData(); - formData.append('backup_id', backupID); - formData.append('user_keys_hash', toBase64URL(userKeysHash)); - formData.append('user_keys', backup.userKeys); - formData.append('user_data_hash', toBase64URL(userDataHash)); - formData.append('user_data', backup.userData); - formData.append('attachments', ''); - return formData; -} - -function getBackupAuthorizationHeader(auth: BackupAuth) { - const authStr = JSON.stringify(auth); - const authBase64 = base64.encode(authStr); - return `Bearer ${authBase64}`; -} - -async function uploadBackup(backup: BackupEncrypted, auth: BackupAuth) { - const authHeader = getBackupAuthorizationHeader(auth); - - const uploadBackupEndpoint = backupService.httpEndpoints.UPLOAD_BACKUP; - const sendBackupResponse = await fetch( - makeBackupServiceEndpointURL(uploadBackupEndpoint), - { - method: uploadBackupEndpoint.method, - body: getBackupFormData(backup), - headers: { - 'Content-Type': 'multipart/form-data', - 'Authorization': authHeader, - }, - }, - ); - - handleHTTPResponseError(sendBackupResponse); -} - -async function getBackupID(username: string): Promise { - const getBackupIDEndpoint = backupService.httpEndpoints.GET_LATEST_BACKUP_ID; - const getBackupIDResponse = await fetch( - makeBackupServiceEndpointURL(getBackupIDEndpoint, { username }), - { - method: getBackupIDEndpoint.method, - }, - ); - - handleHTTPResponseError(getBackupIDResponse); - - const { backupID } = await getBackupIDResponse.json(); - return backupID; -} - -async function getUserKeys( - backupID: string, - auth: BackupAuth, -): Promise { - const authHeader = getBackupAuthorizationHeader(auth); - - const getUserKeysEndpoint = backupService.httpEndpoints.GET_USER_KEYS; - const getUserKeysResponse = await fetch( - makeBackupServiceEndpointURL(getUserKeysEndpoint, { backupID }), - { - method: getUserKeysEndpoint.method, - headers: { - Authorization: authHeader, - }, - }, - ); - - handleHTTPResponseError(getUserKeysResponse); - - const blob = await getUserKeysResponse.blob(); - return getBackupStringFromBlob(blob); -} - -async function getUserData( - backupID: string, - auth: BackupAuth, -): Promise { - const authHeader = getBackupAuthorizationHeader(auth); - - const getUserDataEndpoint = backupService.httpEndpoints.GET_USER_DATA; - const getUserDataResponse = await fetch( - makeBackupServiceEndpointURL(getUserDataEndpoint, { backupID }), - { - method: getUserDataEndpoint.method, - headers: { - Authorization: authHeader, - }, - }, - ); - - handleHTTPResponseError(getUserDataResponse); - - const blob = await getUserDataResponse.blob(); - return getBackupStringFromBlob(blob); -} - -export { uploadBackup, getBackupID, getUserKeys, getUserData }; diff --git a/native/backup/backup-handler.js b/native/backup/backup-handler.js index eeed4e9fe..f3cd85b65 100644 --- a/native/backup/backup-handler.js +++ b/native/backup/backup-handler.js @@ -1,85 +1,92 @@ // @flow import AsyncStorage from '@react-native-async-storage/async-storage'; import * as React from 'react'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { BACKUP_HASH_STORAGE_KEY } from './constants.js'; import { convertObjToBytes } from './conversion-utils.js'; import { useClientBackup } from './use-client-backup.js'; import { commUtilsModule } from '../native-modules.js'; import { useSelector } from '../redux/redux-utils.js'; import Alert from '../utils/alert.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; function BackupHandler(): null { const userStore = useSelector(state => state.userStore); const currentUserID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const isBackupEnabled = useSelector( state => state.localSettings.isBackupEnabled, ); const loggedIn = useSelector(isLoggedIn); const staffCanSee = useStaffCanSee(); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); const { uploadBackupProtocol } = useClientBackup(); + React.useEffect(() => { + if (!isBackupEnabled) { + AsyncStorage.removeItem(BACKUP_HASH_STORAGE_KEY); + } + }, [isBackupEnabled]); + React.useEffect(() => { void (async () => { if ( !isBackupEnabled || !loggedIn || !staffCanSee || !isAccountWithPassword ) { return; } const userData = { userStore }; const userDataBytes = convertObjToBytes(userData); const currentBackupHash = commUtilsModule.sha256(userDataBytes.buffer); const mostRecentlyUploadedBackupHash = await AsyncStorage.getItem( BACKUP_HASH_STORAGE_KEY, ); if ( !mostRecentlyUploadedBackupHash || currentBackupHash !== mostRecentlyUploadedBackupHash ) { try { await uploadBackupProtocol(userData); await AsyncStorage.setItem( BACKUP_HASH_STORAGE_KEY, currentBackupHash, ); } catch (e) { - console.error(`Backup uploading error: ${e}`); + const message = String(getMessageForException(e)); + console.error(`Backup uploading error: ${message}`); Alert.alert( 'Backup protocol info', - `Backup uploading error: ${String(getMessageForException(e))}`, + `Backup uploading error: ${message}`, ); } } })(); }, [ currentUserID, isBackupEnabled, staffCanSee, loggedIn, uploadBackupProtocol, userStore, isAccountWithPassword, ]); return null; } export default BackupHandler; diff --git a/native/backup/conversion-utils.js b/native/backup/conversion-utils.js index 864d2465f..9f7a02f84 100644 --- a/native/backup/conversion-utils.js +++ b/native/backup/conversion-utils.js @@ -1,22 +1,16 @@ // @flow import { commUtilsModule } from '../native-modules.js'; -import { arrayBufferFromBlob } from '../utils/blob-utils-module.js'; - -function getBackupStringFromBlob(blob: Blob): string { - const buffer = arrayBufferFromBlob(blob); - return commUtilsModule.decodeUTF8ArrayBufferToString(buffer); -} function convertObjToBytes(obj: T): Uint8Array { const objStr = JSON.stringify(obj); const objBuffer = commUtilsModule.encodeStringToUTF8ArrayBuffer(objStr ?? ''); return new Uint8Array(objBuffer); } function convertBytesToObj(bytes: Uint8Array): T { const str = commUtilsModule.decodeUTF8ArrayBufferToString(bytes.buffer); return JSON.parse(str); } -export { getBackupStringFromBlob, convertObjToBytes, convertBytesToObj }; +export { convertObjToBytes, convertBytesToObj }; diff --git a/native/backup/use-client-backup.js b/native/backup/use-client-backup.js index 1102bcd25..9ddea4392 100644 --- a/native/backup/use-client-backup.js +++ b/native/backup/use-client-backup.js @@ -1,79 +1,83 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; -import type { UserData } from 'lib/types/backup-types.js'; +import type { UserStore } from 'lib/types/user-types.js'; import { fetchNativeKeychainCredentials } from '../account/native-credentials.js'; import { commCoreModule } from '../native-modules.js'; import { useSelector } from '../redux/redux-utils.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; +type UserData = { + +userStore: UserStore, +}; + type ClientBackup = { +uploadBackupProtocol: (userData: UserData) => Promise, +restoreBackupProtocol: ( expectedUserData: UserData, ) => Promise<{ +dataIntegritySuccess: boolean }>, }; async function getBackupSecret(): Promise { const nativeCredentials = await fetchNativeKeychainCredentials(); if (!nativeCredentials) { throw new Error('Native credentials are missing'); } return nativeCredentials.password; } function useClientBackup(): ClientBackup { const accessToken = useSelector(state => state.commServicesAccessToken); const currentUserID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const loggedIn = useSelector(isLoggedIn); const uploadBackupProtocol = React.useCallback( async (userData: UserData) => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to upload backup for not logged in user.'); } console.info('Start uploading backup...'); const ed25519 = await getContentSigningKey(); await commCoreModule.setCommServicesAuthMetadata( currentUserID, ed25519, accessToken ? accessToken : '', ); const backupSecret = await getBackupSecret(); await commCoreModule.createNewBackup( backupSecret, JSON.stringify(userData), ); console.info('Backup uploaded.'); }, [accessToken, currentUserID, loggedIn], ); const restoreBackupProtocol = React.useCallback( async (expectedUserData: UserData) => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to restore backup for not logged in user.'); } const backupSecret = await getBackupSecret(); const restoreResultStr = await commCoreModule.restoreBackup(backupSecret); const { userData }: { userData: UserData } = JSON.parse(restoreResultStr); return { dataIntegritySuccess: !!_isEqual(userData, expectedUserData) }; }, [currentUserID, loggedIn], ); return { uploadBackupProtocol, restoreBackupProtocol }; } export { useClientBackup }; diff --git a/native/profile/backup-menu.react.js b/native/profile/backup-menu.react.js index df1a50744..66390c522 100644 --- a/native/profile/backup-menu.react.js +++ b/native/profile/backup-menu.react.js @@ -1,130 +1,130 @@ // @flow import * as React from 'react'; import { Alert, Switch, Text, View } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { getMessageForException } from 'lib/utils/errors.js'; import { entries } from 'lib/utils/objects.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 Button from '../components/button.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { setLocalSettingsActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; import { useColors, useStyles } from '../themes/colors.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 userStore = useSelector(state => state.userStore); const isBackupEnabled = useSelector( state => state.localSettings.isBackupEnabled, ); const { restoreBackupProtocol } = useClientBackup(); const testRestore = React.useCallback(async () => { let message; try { const result = await restoreBackupProtocol({ userStore }); message = entries(result) .map(([key, value]) => `${key}: ${String(value)}`) .join('\n'); } catch (e) { - console.error(`Backup uploading error: ${e}`); message = `Backup restore error: ${String(getMessageForException(e))}`; + console.error(message); } Alert.alert('Restore protocol result', message); }, [restoreBackupProtocol, userStore]); const onBackupToggled = React.useCallback( (value: boolean) => { dispatch({ type: setLocalSettingsActionType, payload: { isBackupEnabled: value }, }); }, [dispatch], ); 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;