Be resilient to invalid incrementalMac value

This commit is contained in:
Fedor Indutny 2025-04-16 11:49:49 -07:00 committed by GitHub
commit 53b16c7484
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 71 additions and 41 deletions

View file

@ -192,17 +192,19 @@ export default {
), ),
validateBackup: async () => { validateBackup: async () => {
return { return {
totalBytes: 100, result: {
stats: { totalBytes: 100,
adHocCalls: 1, stats: {
callLinks: 2, adHocCalls: 1,
conversations: 3, callLinks: 2,
chats: 4, conversations: 3,
distributionLists: 5, chats: 4,
messages: 6, distributionLists: 5,
skippedMessages: 7, messages: 6,
stickerPacks: 8, skippedMessages: 7,
fixedDirectMessages: 9, stickerPacks: 8,
fixedDirectMessages: 9,
},
}, },
}; };
}, },

View file

@ -14,7 +14,7 @@ import classNames from 'classnames';
import * as LocaleMatcher from '@formatjs/intl-localematcher'; import * as LocaleMatcher from '@formatjs/intl-localematcher';
import type { MediaDeviceSettings } from '../types/Calling'; import type { MediaDeviceSettings } from '../types/Calling';
import type { ExportResultType as BackupExportResultType } from '../services/backups'; import type { ValidationResultType as BackupValidationResultType } from '../services/backups';
import type { import type {
AutoDownloadAttachmentType, AutoDownloadAttachmentType,
NotificationSettingType, NotificationSettingType,
@ -178,7 +178,7 @@ type PropsFunctionType = {
value: CustomColorType; value: CustomColorType;
} }
) => unknown; ) => unknown;
validateBackup: () => Promise<BackupExportResultType>; validateBackup: () => Promise<BackupValidationResultType>;
// Change handlers // Change handlers
onAudioNotificationsChange: CheckboxChangeHandlerType; onAudioNotificationsChange: CheckboxChangeHandlerType;

View file

@ -5,7 +5,7 @@ import React, { useState, useCallback } from 'react';
import type { LocalizerType } from '../types/I18N'; import type { LocalizerType } from '../types/I18N';
import { toLogFormat } from '../types/errors'; import { toLogFormat } from '../types/errors';
import { formatFileSize } from '../util/formatFileSize'; 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 { SettingsRow, SettingsControl } from './PreferencesUtil';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonVariant } from './Button';
import { Spinner } from './Spinner'; import { Spinner } from './Spinner';
@ -15,26 +15,20 @@ export function PreferencesInternal({
validateBackup: doValidateBackup, validateBackup: doValidateBackup,
}: { }: {
i18n: LocalizerType; i18n: LocalizerType;
validateBackup: () => Promise<BackupExportResultType>; validateBackup: () => Promise<BackupValidationResultType>;
}): JSX.Element { }): JSX.Element {
const [isValidationPending, setIsValidationPending] = useState(false); const [isValidationPending, setIsValidationPending] = useState(false);
const [validationResult, setValidationResult] = useState< const [validationResult, setValidationResult] = useState<
| { BackupValidationResultType | undefined
result: BackupExportResultType;
}
| {
error: Error;
}
| undefined
>(); >();
const validateBackup = useCallback(async () => { const validateBackup = useCallback(async () => {
setIsValidationPending(true); setIsValidationPending(true);
setValidationResult(undefined); setValidationResult(undefined);
try { try {
setValidationResult({ result: await doValidateBackup() }); setValidationResult(await doValidateBackup());
} catch (error) { } catch (error) {
setValidationResult({ error }); setValidationResult({ error: toLogFormat(error) });
} finally { } finally {
setIsValidationPending(false); setIsValidationPending(false);
} }
@ -61,7 +55,7 @@ export function PreferencesInternal({
validationElem = ( validationElem = (
<div className="Preferences--internal--validate-backup--error"> <div className="Preferences--internal--validate-backup--error">
<pre> <pre>
<code>{toLogFormat(error)}</code> <code>{error}</code>
</pre> </pre>
</div> </div>
); );

View file

@ -52,7 +52,7 @@ import { BackupImportStream } from './import';
import { getKeyMaterial } from './crypto'; import { getKeyMaterial } from './crypto';
import { BackupCredentials } from './credentials'; import { BackupCredentials } from './credentials';
import { BackupAPI } from './api'; import { BackupAPI } from './api';
import { validateBackup } from './validator'; import { validateBackup, ValidationType } from './validator';
import { BackupType } from './types'; import { BackupType } from './types';
import { import {
BackupInstallerError, BackupInstallerError,
@ -103,6 +103,15 @@ export type ExportResultType = Readonly<{
stats: Readonly<StatsType>; stats: Readonly<StatsType>;
}>; }>;
export type ValidationResultType = Readonly<
| {
result: ExportResultType;
}
| {
error: string;
}
>;
export class BackupsService { export class BackupsService {
#isStarted = false; #isStarted = false;
#isRunning: 'import' | 'export' | false = false; #isRunning: 'import' | 'export' | false = false;
@ -322,26 +331,40 @@ export class BackupsService {
); );
if (backupType === BackupType.Ciphertext) { if (backupType === BackupType.Ciphertext) {
await validateBackup(() => new FileStream(path), totalBytes); await validateBackup(
() => new FileStream(path),
totalBytes,
isTestOrMockEnvironment()
? ValidationType.Internal
: ValidationType.Export
);
} }
return totalBytes; return totalBytes;
} }
// Test harness // Test harness
public async validate( public async _internalValidate(
backupLevel: BackupLevel = BackupLevel.Free, backupLevel: BackupLevel = BackupLevel.Free,
backupType = BackupType.Ciphertext backupType = BackupType.Ciphertext
): Promise<ExportResultType> { ): Promise<ValidationResultType> {
const { data, ...result } = await this.exportBackupData( const { data, ...result } = await this.exportBackupData(
backupLevel, backupLevel,
backupType backupType
); );
const buffer = Buffer.from(data); 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 // Test harness

View file

@ -180,9 +180,11 @@ export async function getFilePointerForAttachment({
}> { }> {
const filePointerRootProps = new Backups.FilePointer({ const filePointerRootProps = new Backups.FilePointer({
contentType: attachment.contentType, contentType: attachment.contentType,
incrementalMac: attachment.incrementalMac // Resilience to invalid data in the database from internal testing
? Bytes.fromBase64(attachment.incrementalMac) incrementalMac:
: undefined, typeof attachment.incrementalMac === 'string'
? Bytes.fromBase64(attachment.incrementalMac)
: undefined,
incrementalMacChunkSize: dropZero(attachment.chunkSize), incrementalMacChunkSize: dropZero(attachment.chunkSize),
fileName: attachment.fileName, fileName: attachment.fileName,
width: attachment.width, width: attachment.width,

View file

@ -6,11 +6,17 @@ import type { InputStream } from '@signalapp/libsignal-client/dist/io';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { toAciObject } from '../../util/ServiceId'; 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( export async function validateBackup(
inputFactory: () => InputStream, inputFactory: () => InputStream,
fileSize: number fileSize: number,
type: ValidationType
): Promise<void> { ): Promise<void> {
const accountEntropy = window.storage.get('accountEntropyPool'); const accountEntropy = window.storage.get('accountEntropyPool');
strictAssert(accountEntropy, 'Account Entropy Pool not available'); strictAssert(accountEntropy, 'Account Entropy Pool not available');
@ -28,12 +34,14 @@ export async function validateBackup(
BigInt(fileSize) BigInt(fileSize)
); );
if (isTestOrMockEnvironment()) { if (type === ValidationType.Internal) {
strictAssert( strictAssert(
outcome.ok, outcome.ok,
`Backup validation failed: ${outcome.errorMessage}` `Backup validation failed: ${outcome.errorMessage}`
); );
} else { } else if (type === ValidationType.Export) {
strictAssert(outcome.ok, 'Backup validation failed'); strictAssert(outcome.ok, 'Backup validation failed');
} else {
throw missingCaseError(type);
} }
} }

View file

@ -25,7 +25,7 @@ import { resolveUsernameByLinkBase64 } from '../services/username';
import { writeProfile } from '../services/writeProfile'; import { writeProfile } from '../services/writeProfile';
import { import {
backupsService, backupsService,
type ExportResultType as BackupExportResultType, type ValidationResultType as BackupValidationResultType,
} from '../services/backups'; } from '../services/backups';
import { isInCall } from '../state/selectors/calling'; import { isInCall } from '../state/selectors/calling';
import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations'; import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations';
@ -164,7 +164,7 @@ export type IPCEventsCallbacksType = {
unknownSignalLink: () => void; unknownSignalLink: () => void;
getCustomColors: () => Record<string, CustomColorType>; getCustomColors: () => Record<string, CustomColorType>;
syncRequest: () => Promise<void>; syncRequest: () => Promise<void>;
validateBackup: () => Promise<BackupExportResultType>; validateBackup: () => Promise<BackupValidationResultType>;
setGlobalDefaultConversationColor: ( setGlobalDefaultConversationColor: (
color: ConversationColorType, color: ConversationColorType,
customColor?: { id: string; value: CustomColorType } customColor?: { id: string; value: CustomColorType }
@ -551,7 +551,8 @@ export function createIPCEvents(
await sendSyncRequests(); await sendSyncRequests();
return contactSyncComplete; return contactSyncComplete;
}, },
validateBackup: () => backupsService.validate(), // Only for internal use
validateBackup: () => backupsService._internalValidate(),
getLastSyncTime: () => window.storage.get('synced_at'), getLastSyncTime: () => window.storage.get('synced_at'),
setLastSyncTime: value => window.storage.put('synced_at', value), setLastSyncTime: value => window.storage.put('synced_at', value),
getUniversalExpireTimer: () => universalExpireTimer.get(), getUniversalExpireTimer: () => universalExpireTimer.get(),