diff --git a/ts/components/Preferences.stories.tsx b/ts/components/Preferences.stories.tsx index b21f27c22a11..c737e40bf65f 100644 --- a/ts/components/Preferences.stories.tsx +++ b/ts/components/Preferences.stories.tsx @@ -192,17 +192,19 @@ export default { ), validateBackup: async () => { return { - totalBytes: 100, - stats: { - adHocCalls: 1, - callLinks: 2, - conversations: 3, - chats: 4, - distributionLists: 5, - messages: 6, - skippedMessages: 7, - stickerPacks: 8, - fixedDirectMessages: 9, + result: { + totalBytes: 100, + stats: { + adHocCalls: 1, + callLinks: 2, + conversations: 3, + chats: 4, + distributionLists: 5, + messages: 6, + skippedMessages: 7, + stickerPacks: 8, + fixedDirectMessages: 9, + }, }, }; }, diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx index 634776474cbf..f05cb07adc3e 100644 --- a/ts/components/Preferences.tsx +++ b/ts/components/Preferences.tsx @@ -14,7 +14,7 @@ import classNames from 'classnames'; import * as LocaleMatcher from '@formatjs/intl-localematcher'; import type { MediaDeviceSettings } from '../types/Calling'; -import type { ExportResultType as BackupExportResultType } from '../services/backups'; +import type { ValidationResultType as BackupValidationResultType } from '../services/backups'; import type { AutoDownloadAttachmentType, NotificationSettingType, @@ -178,7 +178,7 @@ type PropsFunctionType = { value: CustomColorType; } ) => unknown; - validateBackup: () => Promise; + validateBackup: () => Promise; // Change handlers onAudioNotificationsChange: CheckboxChangeHandlerType; diff --git a/ts/components/PreferencesInternal.tsx b/ts/components/PreferencesInternal.tsx index 076ff3bfee92..8a72e10aa090 100644 --- a/ts/components/PreferencesInternal.tsx +++ b/ts/components/PreferencesInternal.tsx @@ -5,7 +5,7 @@ import React, { useState, useCallback } from 'react'; import type { LocalizerType } from '../types/I18N'; import { toLogFormat } from '../types/errors'; import { formatFileSize } from '../util/formatFileSize'; -import type { ExportResultType as BackupExportResultType } from '../services/backups'; +import type { ValidationResultType as BackupValidationResultType } from '../services/backups'; import { SettingsRow, SettingsControl } from './PreferencesUtil'; import { Button, ButtonVariant } from './Button'; import { Spinner } from './Spinner'; @@ -15,26 +15,20 @@ export function PreferencesInternal({ validateBackup: doValidateBackup, }: { i18n: LocalizerType; - validateBackup: () => Promise; + validateBackup: () => Promise; }): JSX.Element { const [isValidationPending, setIsValidationPending] = useState(false); const [validationResult, setValidationResult] = useState< - | { - result: BackupExportResultType; - } - | { - error: Error; - } - | undefined + BackupValidationResultType | undefined >(); const validateBackup = useCallback(async () => { setIsValidationPending(true); setValidationResult(undefined); try { - setValidationResult({ result: await doValidateBackup() }); + setValidationResult(await doValidateBackup()); } catch (error) { - setValidationResult({ error }); + setValidationResult({ error: toLogFormat(error) }); } finally { setIsValidationPending(false); } @@ -61,7 +55,7 @@ export function PreferencesInternal({ validationElem = (
-            {toLogFormat(error)}
+            {error}
           
); diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index 009ed1ec0b2e..11ed7aac2f94 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -52,7 +52,7 @@ import { BackupImportStream } from './import'; import { getKeyMaterial } from './crypto'; import { BackupCredentials } from './credentials'; import { BackupAPI } from './api'; -import { validateBackup } from './validator'; +import { validateBackup, ValidationType } from './validator'; import { BackupType } from './types'; import { BackupInstallerError, @@ -103,6 +103,15 @@ export type ExportResultType = Readonly<{ stats: Readonly; }>; +export type ValidationResultType = Readonly< + | { + result: ExportResultType; + } + | { + error: string; + } +>; + export class BackupsService { #isStarted = false; #isRunning: 'import' | 'export' | false = false; @@ -322,26 +331,40 @@ export class BackupsService { ); if (backupType === BackupType.Ciphertext) { - await validateBackup(() => new FileStream(path), totalBytes); + await validateBackup( + () => new FileStream(path), + totalBytes, + isTestOrMockEnvironment() + ? ValidationType.Internal + : ValidationType.Export + ); } return totalBytes; } // Test harness - public async validate( + public async _internalValidate( backupLevel: BackupLevel = BackupLevel.Free, backupType = BackupType.Ciphertext - ): Promise { + ): Promise { const { data, ...result } = await this.exportBackupData( backupLevel, backupType ); const buffer = Buffer.from(data); - await validateBackup(() => new MemoryStream(buffer), buffer.byteLength); + try { + await validateBackup( + () => new MemoryStream(buffer), + buffer.byteLength, + ValidationType.Internal + ); - return result; + return { result }; + } catch (error) { + return { error: Errors.toLogFormat(error) }; + } } // Test harness diff --git a/ts/services/backups/util/filePointers.ts b/ts/services/backups/util/filePointers.ts index 8375ab9b5dc5..2e7104ba319a 100644 --- a/ts/services/backups/util/filePointers.ts +++ b/ts/services/backups/util/filePointers.ts @@ -180,9 +180,11 @@ export async function getFilePointerForAttachment({ }> { const filePointerRootProps = new Backups.FilePointer({ contentType: attachment.contentType, - incrementalMac: attachment.incrementalMac - ? Bytes.fromBase64(attachment.incrementalMac) - : undefined, + // Resilience to invalid data in the database from internal testing + incrementalMac: + typeof attachment.incrementalMac === 'string' + ? Bytes.fromBase64(attachment.incrementalMac) + : undefined, incrementalMacChunkSize: dropZero(attachment.chunkSize), fileName: attachment.fileName, width: attachment.width, diff --git a/ts/services/backups/validator.ts b/ts/services/backups/validator.ts index 599a8677336f..73cb24fb1ea3 100644 --- a/ts/services/backups/validator.ts +++ b/ts/services/backups/validator.ts @@ -6,11 +6,17 @@ import type { InputStream } from '@signalapp/libsignal-client/dist/io'; import { strictAssert } from '../../util/assert'; import { toAciObject } from '../../util/ServiceId'; -import { isTestOrMockEnvironment } from '../../environment'; +import { missingCaseError } from '../../util/missingCaseError'; + +export enum ValidationType { + Export = 'Export', + Internal = 'Internal', +} export async function validateBackup( inputFactory: () => InputStream, - fileSize: number + fileSize: number, + type: ValidationType ): Promise { const accountEntropy = window.storage.get('accountEntropyPool'); strictAssert(accountEntropy, 'Account Entropy Pool not available'); @@ -28,12 +34,14 @@ export async function validateBackup( BigInt(fileSize) ); - if (isTestOrMockEnvironment()) { + if (type === ValidationType.Internal) { strictAssert( outcome.ok, `Backup validation failed: ${outcome.errorMessage}` ); - } else { + } else if (type === ValidationType.Export) { strictAssert(outcome.ok, 'Backup validation failed'); + } else { + throw missingCaseError(type); } } diff --git a/ts/util/createIPCEvents.ts b/ts/util/createIPCEvents.ts index 5057800f8770..44df51dabf58 100644 --- a/ts/util/createIPCEvents.ts +++ b/ts/util/createIPCEvents.ts @@ -25,7 +25,7 @@ import { resolveUsernameByLinkBase64 } from '../services/username'; import { writeProfile } from '../services/writeProfile'; import { backupsService, - type ExportResultType as BackupExportResultType, + type ValidationResultType as BackupValidationResultType, } from '../services/backups'; import { isInCall } from '../state/selectors/calling'; import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations'; @@ -164,7 +164,7 @@ export type IPCEventsCallbacksType = { unknownSignalLink: () => void; getCustomColors: () => Record; syncRequest: () => Promise; - validateBackup: () => Promise; + validateBackup: () => Promise; setGlobalDefaultConversationColor: ( color: ConversationColorType, customColor?: { id: string; value: CustomColorType } @@ -551,7 +551,8 @@ export function createIPCEvents( await sendSyncRequests(); return contactSyncComplete; }, - validateBackup: () => backupsService.validate(), + // Only for internal use + validateBackup: () => backupsService._internalValidate(), getLastSyncTime: () => window.storage.get('synced_at'), setLastSyncTime: value => window.storage.put('synced_at', value), getUniversalExpireTimer: () => universalExpireTimer.get(),