Export/import attachments in integration tests
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
parent
850b78042b
commit
6859b1a220
21 changed files with 292 additions and 116 deletions
|
@ -4699,6 +4699,10 @@
|
||||||
"messageformat": "Downloading {currentSize} of {totalSize} ({fractionComplete, number, percent})...",
|
"messageformat": "Downloading {currentSize} of {totalSize} ({fractionComplete, number, percent})...",
|
||||||
"description": "Hint under the progressbar in the backup import screen"
|
"description": "Hint under the progressbar in the backup import screen"
|
||||||
},
|
},
|
||||||
|
"icu:BackupImportScreen__progressbar-hint--processing": {
|
||||||
|
"messageformat": "Finalizing message transfer...",
|
||||||
|
"description": "Hint under the progressbar in the backup import screen shown after the backup file is downloaded and while backup data is being processed"
|
||||||
|
},
|
||||||
"icu:BackupImportScreen__progressbar-hint--preparing": {
|
"icu:BackupImportScreen__progressbar-hint--preparing": {
|
||||||
"messageformat": "Preparing to download...",
|
"messageformat": "Preparing to download...",
|
||||||
"description": "Hint under the progressbar in the backup import screen when download size is not yet known"
|
"description": "Hint under the progressbar in the backup import screen when download size is not yet known"
|
||||||
|
|
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -126,7 +126,7 @@
|
||||||
"@indutny/parallel-prettier": "3.0.0",
|
"@indutny/parallel-prettier": "3.0.0",
|
||||||
"@indutny/rezip-electron": "1.3.1",
|
"@indutny/rezip-electron": "1.3.1",
|
||||||
"@indutny/symbolicate-mac": "2.3.0",
|
"@indutny/symbolicate-mac": "2.3.0",
|
||||||
"@signalapp/mock-server": "8.0.1",
|
"@signalapp/mock-server": "8.1.1",
|
||||||
"@storybook/addon-a11y": "8.1.11",
|
"@storybook/addon-a11y": "8.1.11",
|
||||||
"@storybook/addon-actions": "8.1.11",
|
"@storybook/addon-actions": "8.1.11",
|
||||||
"@storybook/addon-controls": "8.1.11",
|
"@storybook/addon-controls": "8.1.11",
|
||||||
|
@ -7296,9 +7296,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@signalapp/mock-server": {
|
"node_modules/@signalapp/mock-server": {
|
||||||
"version": "8.0.1",
|
"version": "8.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@signalapp/mock-server/-/mock-server-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@signalapp/mock-server/-/mock-server-8.1.1.tgz",
|
||||||
"integrity": "sha512-qfyBOtMmQ3RF3Kig0DTafrxUx8MZ2hB+5H6ZJVV1lQS022U6bOHiVjZyAJ0uZgU98FJZIXlT/zWJ24kFl6/pGQ==",
|
"integrity": "sha512-TlQpOyUYnDBV7boxyaLDaeGTN5WIn4trbF+9rq4+6rXfpzIBnf2A4Y1fzFRVL9F9/F4ZEPtrv+V3oplNrfoZ9w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -210,7 +210,7 @@
|
||||||
"@indutny/parallel-prettier": "3.0.0",
|
"@indutny/parallel-prettier": "3.0.0",
|
||||||
"@indutny/rezip-electron": "1.3.1",
|
"@indutny/rezip-electron": "1.3.1",
|
||||||
"@indutny/symbolicate-mac": "2.3.0",
|
"@indutny/symbolicate-mac": "2.3.0",
|
||||||
"@signalapp/mock-server": "8.0.1",
|
"@signalapp/mock-server": "8.1.1",
|
||||||
"@storybook/addon-a11y": "8.1.11",
|
"@storybook/addon-a11y": "8.1.11",
|
||||||
"@storybook/addon-actions": "8.1.11",
|
"@storybook/addon-actions": "8.1.11",
|
||||||
"@storybook/addon-controls": "8.1.11",
|
"@storybook/addon-controls": "8.1.11",
|
||||||
|
|
|
@ -110,17 +110,22 @@ export type HardcodedIVForEncryptionType =
|
||||||
digestToMatch: Uint8Array;
|
digestToMatch: Uint8Array;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EncryptAttachmentV2PropsType = {
|
type EncryptAttachmentV2OptionsType = Readonly<{
|
||||||
dangerousIv?: HardcodedIVForEncryptionType;
|
dangerousIv?: HardcodedIVForEncryptionType;
|
||||||
dangerousTestOnlySkipPadding?: boolean;
|
dangerousTestOnlySkipPadding?: boolean;
|
||||||
getAbsoluteAttachmentPath: (relativePath: string) => string;
|
|
||||||
keys: Readonly<Uint8Array>;
|
keys: Readonly<Uint8Array>;
|
||||||
needIncrementalMac: boolean;
|
needIncrementalMac: boolean;
|
||||||
plaintext: PlaintextSourceType;
|
plaintext: PlaintextSourceType;
|
||||||
};
|
}>;
|
||||||
|
|
||||||
|
export type EncryptAttachmentV2ToDiskOptionsType =
|
||||||
|
EncryptAttachmentV2OptionsType &
|
||||||
|
Readonly<{
|
||||||
|
getAbsoluteAttachmentPath: (relativePath: string) => string;
|
||||||
|
}>;
|
||||||
|
|
||||||
export async function encryptAttachmentV2ToDisk(
|
export async function encryptAttachmentV2ToDisk(
|
||||||
args: EncryptAttachmentV2PropsType
|
args: EncryptAttachmentV2ToDiskOptionsType
|
||||||
): Promise<EncryptedAttachmentV2 & { path: string }> {
|
): Promise<EncryptedAttachmentV2 & { path: string }> {
|
||||||
// Create random output file
|
// Create random output file
|
||||||
const relativeTargetPath = getRelativePath(createName());
|
const relativeTargetPath = getRelativePath(createName());
|
||||||
|
@ -152,7 +157,7 @@ export async function encryptAttachmentV2({
|
||||||
needIncrementalMac,
|
needIncrementalMac,
|
||||||
plaintext,
|
plaintext,
|
||||||
sink,
|
sink,
|
||||||
}: EncryptAttachmentV2PropsType & {
|
}: EncryptAttachmentV2OptionsType & {
|
||||||
sink?: Writable;
|
sink?: Writable;
|
||||||
}): Promise<EncryptedAttachmentV2> {
|
}): Promise<EncryptedAttachmentV2> {
|
||||||
const logId = 'encryptAttachmentV2';
|
const logId = 'encryptAttachmentV2';
|
||||||
|
@ -580,7 +585,6 @@ export async function decryptAndReencryptLocally(
|
||||||
const [result] = await Promise.all([
|
const [result] = await Promise.all([
|
||||||
decryptAttachmentV2ToSink(options, passthrough),
|
decryptAttachmentV2ToSink(options, passthrough),
|
||||||
await encryptAttachmentV2({
|
await encryptAttachmentV2({
|
||||||
getAbsoluteAttachmentPath: options.getAbsoluteAttachmentPath,
|
|
||||||
keys,
|
keys,
|
||||||
needIncrementalMac: false,
|
needIncrementalMac: false,
|
||||||
plaintext: {
|
plaintext: {
|
||||||
|
|
2
ts/CI.ts
2
ts/CI.ts
|
@ -10,6 +10,7 @@ import * as log from './logging/log';
|
||||||
import { explodePromise } from './util/explodePromise';
|
import { explodePromise } from './util/explodePromise';
|
||||||
import { AccessType, ipcInvoke } from './sql/channels';
|
import { AccessType, ipcInvoke } from './sql/channels';
|
||||||
import { backupsService } from './services/backups';
|
import { backupsService } from './services/backups';
|
||||||
|
import { AttachmentBackupManager } from './jobs/AttachmentBackupManager';
|
||||||
import { SECOND } from './util/durations';
|
import { SECOND } from './util/durations';
|
||||||
import { isSignalRoute } from './util/signalRoutes';
|
import { isSignalRoute } from './util/signalRoutes';
|
||||||
import { strictAssert } from './util/assert';
|
import { strictAssert } from './util/assert';
|
||||||
|
@ -168,6 +169,7 @@ export function getCI({ deviceName }: GetCIOptionsType): CIType {
|
||||||
|
|
||||||
async function uploadBackup() {
|
async function uploadBackup() {
|
||||||
await backupsService.upload();
|
await backupsService.upload();
|
||||||
|
await AttachmentBackupManager.waitForIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
function unlink() {
|
function unlink() {
|
||||||
|
|
|
@ -1604,8 +1604,9 @@ export async function startApp(): Promise<void> {
|
||||||
// Download backup before enabling request handler and storage service
|
// Download backup before enabling request handler and storage service
|
||||||
try {
|
try {
|
||||||
await backupsService.download({
|
await backupsService.download({
|
||||||
onProgress: (currentBytes, totalBytes) => {
|
onProgress: (backupStep, currentBytes, totalBytes) => {
|
||||||
window.reduxActions.installer.updateBackupImportProgress({
|
window.reduxActions.installer.updateBackupImportProgress({
|
||||||
|
backupStep,
|
||||||
currentBytes,
|
currentBytes,
|
||||||
totalBytes,
|
totalBytes,
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import React from 'react';
|
||||||
import type { Meta, StoryFn } from '@storybook/react';
|
import type { Meta, StoryFn } from '@storybook/react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { setupI18n } from '../../util/setupI18n';
|
import { setupI18n } from '../../util/setupI18n';
|
||||||
|
import { InstallScreenBackupStep } from '../../types/InstallScreen';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import type { PropsType } from './InstallScreenBackupImportStep';
|
import type { PropsType } from './InstallScreenBackupImportStep';
|
||||||
import { InstallScreenBackupImportStep } from './InstallScreenBackupImportStep';
|
import { InstallScreenBackupImportStep } from './InstallScreenBackupImportStep';
|
||||||
|
@ -27,25 +28,36 @@ const Template: StoryFn<PropsType> = (args: PropsType) => (
|
||||||
|
|
||||||
export const NoBytes = Template.bind({});
|
export const NoBytes = Template.bind({});
|
||||||
NoBytes.args = {
|
NoBytes.args = {
|
||||||
|
backupStep: InstallScreenBackupStep.Download,
|
||||||
currentBytes: undefined,
|
currentBytes: undefined,
|
||||||
totalBytes: undefined,
|
totalBytes: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Bytes = Template.bind({});
|
export const Bytes = Template.bind({});
|
||||||
Bytes.args = {
|
Bytes.args = {
|
||||||
|
backupStep: InstallScreenBackupStep.Download,
|
||||||
currentBytes: 500 * 1024,
|
currentBytes: 500 * 1024,
|
||||||
totalBytes: 1024 * 1024,
|
totalBytes: 1024 * 1024,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Full = Template.bind({});
|
export const Full = Template.bind({});
|
||||||
Full.args = {
|
Full.args = {
|
||||||
|
backupStep: InstallScreenBackupStep.Download,
|
||||||
currentBytes: 1024,
|
currentBytes: 1024,
|
||||||
totalBytes: 1024,
|
totalBytes: 1024,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Error = Template.bind({});
|
export const Error = Template.bind({});
|
||||||
Error.args = {
|
Error.args = {
|
||||||
|
backupStep: InstallScreenBackupStep.Download,
|
||||||
currentBytes: 500 * 1024,
|
currentBytes: 500 * 1024,
|
||||||
totalBytes: 1024 * 1024,
|
totalBytes: 1024 * 1024,
|
||||||
hasError: true,
|
hasError: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Processing = Template.bind({});
|
||||||
|
Processing.args = {
|
||||||
|
backupStep: InstallScreenBackupStep.Process,
|
||||||
|
currentBytes: 500 * 1024,
|
||||||
|
totalBytes: 1024 * 1024,
|
||||||
|
};
|
||||||
|
|
|
@ -4,18 +4,21 @@
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
|
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
|
import { InstallScreenBackupStep } from '../../types/InstallScreen';
|
||||||
import { formatFileSize } from '../../util/formatFileSize';
|
import { formatFileSize } from '../../util/formatFileSize';
|
||||||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||||
import { ProgressBar } from '../ProgressBar';
|
import { ProgressBar } from '../ProgressBar';
|
||||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||||
import { roundFractionForProgressBar } from '../../util/numbers';
|
import { roundFractionForProgressBar } from '../../util/numbers';
|
||||||
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
|
|
||||||
// We can't always use destructuring assignment because of the complexity of this props
|
// We can't always use destructuring assignment because of the complexity of this props
|
||||||
// type.
|
// type.
|
||||||
|
|
||||||
export type PropsType = Readonly<{
|
export type PropsType = Readonly<{
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
backupStep: InstallScreenBackupStep;
|
||||||
currentBytes?: number;
|
currentBytes?: number;
|
||||||
totalBytes?: number;
|
totalBytes?: number;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
|
@ -25,6 +28,7 @@ export type PropsType = Readonly<{
|
||||||
|
|
||||||
export function InstallScreenBackupImportStep({
|
export function InstallScreenBackupImportStep({
|
||||||
i18n,
|
i18n,
|
||||||
|
backupStep,
|
||||||
currentBytes,
|
currentBytes,
|
||||||
totalBytes,
|
totalBytes,
|
||||||
hasError,
|
hasError,
|
||||||
|
@ -66,26 +70,33 @@ export function InstallScreenBackupImportStep({
|
||||||
}, [onRetry]);
|
}, [onRetry]);
|
||||||
|
|
||||||
let progress: JSX.Element;
|
let progress: JSX.Element;
|
||||||
let isCancelPossible = true;
|
|
||||||
if (currentBytes != null && totalBytes != null) {
|
if (currentBytes != null && totalBytes != null) {
|
||||||
isCancelPossible = currentBytes !== totalBytes;
|
|
||||||
|
|
||||||
const fractionComplete = roundFractionForProgressBar(
|
const fractionComplete = roundFractionForProgressBar(
|
||||||
currentBytes / totalBytes
|
currentBytes / totalBytes
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let hint: string;
|
||||||
|
if (backupStep === InstallScreenBackupStep.Download) {
|
||||||
|
hint = i18n('icu:BackupImportScreen__progressbar-hint', {
|
||||||
|
currentSize: formatFileSize(currentBytes),
|
||||||
|
totalSize: formatFileSize(totalBytes),
|
||||||
|
fractionComplete,
|
||||||
|
});
|
||||||
|
} else if (backupStep === InstallScreenBackupStep.Process) {
|
||||||
|
hint = i18n('icu:BackupImportScreen__progressbar-hint--processing');
|
||||||
|
} else {
|
||||||
|
throw missingCaseError(backupStep);
|
||||||
|
}
|
||||||
|
|
||||||
progress = (
|
progress = (
|
||||||
<>
|
<>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
key={backupStep}
|
||||||
fractionComplete={fractionComplete}
|
fractionComplete={fractionComplete}
|
||||||
isRTL={i18n.getLocaleDirection() === 'rtl'}
|
isRTL={i18n.getLocaleDirection() === 'rtl'}
|
||||||
/>
|
/>
|
||||||
<div className="InstallScreenBackupImportStep__progressbar-hint">
|
<div className="InstallScreenBackupImportStep__progressbar-hint">
|
||||||
{i18n('icu:BackupImportScreen__progressbar-hint', {
|
{hint}
|
||||||
currentSize: formatFileSize(currentBytes),
|
|
||||||
totalSize: formatFileSize(totalBytes),
|
|
||||||
fractionComplete,
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -93,6 +104,7 @@ export function InstallScreenBackupImportStep({
|
||||||
progress = (
|
progress = (
|
||||||
<>
|
<>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
key={backupStep}
|
||||||
fractionComplete={0}
|
fractionComplete={0}
|
||||||
isRTL={i18n.getLocaleDirection() === 'rtl'}
|
isRTL={i18n.getLocaleDirection() === 'rtl'}
|
||||||
/>
|
/>
|
||||||
|
@ -119,7 +131,7 @@ export function InstallScreenBackupImportStep({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isCancelPossible && (
|
{backupStep === InstallScreenBackupStep.Download && (
|
||||||
<button
|
<button
|
||||||
className="InstallScreenBackupImportStep__cancel"
|
className="InstallScreenBackupImportStep__cancel"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -159,6 +159,10 @@ export class AttachmentBackupManager extends JobManager<CoreAttachmentBackupJobT
|
||||||
static async addJob(newJob: CoreAttachmentBackupJobType): Promise<void> {
|
static async addJob(newJob: CoreAttachmentBackupJobType): Promise<void> {
|
||||||
return AttachmentBackupManager.instance.addJob(newJob);
|
return AttachmentBackupManager.instance.addJob(newJob);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async waitForIdle(): Promise<void> {
|
||||||
|
return AttachmentBackupManager.instance.waitForIdle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getJobId(job: CoreAttachmentBackupJobType): string {
|
function getJobId(job: CoreAttachmentBackupJobType): string {
|
||||||
|
|
|
@ -77,6 +77,7 @@ export abstract class JobManager<CoreJobType> {
|
||||||
private jobCompletePromises: Map<string, ExplodePromiseResultType<void>> =
|
private jobCompletePromises: Map<string, ExplodePromiseResultType<void>> =
|
||||||
new Map();
|
new Map();
|
||||||
private tickTimeout: NodeJS.Timeout | null = null;
|
private tickTimeout: NodeJS.Timeout | null = null;
|
||||||
|
private idleCallbacks = new Array<() => void>();
|
||||||
|
|
||||||
protected logPrefix = 'JobManager';
|
protected logPrefix = 'JobManager';
|
||||||
public tickInterval = DEFAULT_TICK_INTERVAL;
|
public tickInterval = DEFAULT_TICK_INTERVAL;
|
||||||
|
@ -106,6 +107,14 @@ export abstract class JobManager<CoreJobType> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async waitForIdle(): Promise<void> {
|
||||||
|
if (this.activeJobs.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>(resolve => this.idleCallbacks.push(resolve));
|
||||||
|
}
|
||||||
|
|
||||||
private tick(): void {
|
private tick(): void {
|
||||||
clearTimeoutIfNecessary(this.tickTimeout);
|
clearTimeoutIfNecessary(this.tickTimeout);
|
||||||
this.tickTimeout = null;
|
this.tickTimeout = null;
|
||||||
|
@ -233,6 +242,13 @@ export abstract class JobManager<CoreJobType> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (nextJobs.length === 0) {
|
if (nextJobs.length === 0) {
|
||||||
|
if (this.idleCallbacks.length > 0) {
|
||||||
|
const callbacks = this.idleCallbacks;
|
||||||
|
this.idleCallbacks = [];
|
||||||
|
for (const callback of callbacks) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { getMacAndUpdateHmac } from '../../util/getMacAndUpdateHmac';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { HOUR } from '../../util/durations';
|
import { HOUR } from '../../util/durations';
|
||||||
import { CipherType, HashType } from '../../types/Crypto';
|
import { CipherType, HashType } from '../../types/Crypto';
|
||||||
|
import { InstallScreenBackupStep } from '../../types/InstallScreen';
|
||||||
import * as Errors from '../../types/errors';
|
import * as Errors from '../../types/errors';
|
||||||
import { HTTPError } from '../../textsecure/Errors';
|
import { HTTPError } from '../../textsecure/Errors';
|
||||||
import { constantTimeEqual } from '../../Crypto';
|
import { constantTimeEqual } from '../../Crypto';
|
||||||
|
@ -36,7 +37,7 @@ import { BackupExportStream } from './export';
|
||||||
import { BackupImportStream } from './import';
|
import { BackupImportStream } from './import';
|
||||||
import { getKeyMaterial } from './crypto';
|
import { getKeyMaterial } from './crypto';
|
||||||
import { BackupCredentials } from './credentials';
|
import { BackupCredentials } from './credentials';
|
||||||
import { BackupAPI, type DownloadOptionsType } from './api';
|
import { BackupAPI } from './api';
|
||||||
import { validateBackup } from './validator';
|
import { validateBackup } from './validator';
|
||||||
import { BackupType } from './types';
|
import { BackupType } from './types';
|
||||||
import type { ExplodePromiseResultType } from '../../util/explodePromise';
|
import type { ExplodePromiseResultType } from '../../util/explodePromise';
|
||||||
|
@ -49,6 +50,20 @@ const IV_LENGTH = 16;
|
||||||
|
|
||||||
const BACKUP_REFRESH_INTERVAL = 24 * HOUR;
|
const BACKUP_REFRESH_INTERVAL = 24 * HOUR;
|
||||||
|
|
||||||
|
export type DownloadOptionsType = Readonly<{
|
||||||
|
onProgress?: (
|
||||||
|
backupStep: InstallScreenBackupStep,
|
||||||
|
currentBytes: number,
|
||||||
|
totalBytes: number
|
||||||
|
) => void;
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type ImportOptionsType = Readonly<{
|
||||||
|
backupType?: BackupType;
|
||||||
|
onProgress?: (currentBytes: number, totalBytes: number) => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
export class BackupsService {
|
export class BackupsService {
|
||||||
private isStarted = false;
|
private isStarted = false;
|
||||||
private isRunning = false;
|
private isRunning = false;
|
||||||
|
@ -82,9 +97,7 @@ export class BackupsService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async download(
|
public async download(options: DownloadOptionsType): Promise<void> {
|
||||||
options: Omit<DownloadOptionsType, 'downloadOffset'>
|
|
||||||
): Promise<void> {
|
|
||||||
const backupDownloadPath = window.storage.get('backupDownloadPath');
|
const backupDownloadPath = window.storage.get('backupDownloadPath');
|
||||||
if (!backupDownloadPath) {
|
if (!backupDownloadPath) {
|
||||||
log.warn('backups.download: no backup download path, skipping');
|
log.warn('backups.download: no backup download path, skipping');
|
||||||
|
@ -102,7 +115,10 @@ export class BackupsService {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
hasBackup = await this.doDownload(absoluteDownloadPath, options);
|
hasBackup = await this.doDownload(absoluteDownloadPath, options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.info('backups.download: error, prompting user to retry');
|
log.warn(
|
||||||
|
'backups.download: error, prompting user to retry',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
this.downloadRetryPromise = explodePromise<RetryBackupImportValue>();
|
this.downloadRetryPromise = explodePromise<RetryBackupImportValue>();
|
||||||
window.reduxActions.installer.updateBackupImportProgress({
|
window.reduxActions.installer.updateBackupImportProgress({
|
||||||
hasError: true,
|
hasError: true,
|
||||||
|
@ -202,8 +218,11 @@ export class BackupsService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async importFromDisk(backupFile: string): Promise<void> {
|
public async importFromDisk(
|
||||||
return backupsService.importBackup(() => createReadStream(backupFile));
|
backupFile: string,
|
||||||
|
options?: ImportOptionsType
|
||||||
|
): Promise<void> {
|
||||||
|
return this.importBackup(() => createReadStream(backupFile), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public cancelDownload(): void {
|
public cancelDownload(): void {
|
||||||
|
@ -221,7 +240,7 @@ export class BackupsService {
|
||||||
|
|
||||||
public async importBackup(
|
public async importBackup(
|
||||||
createBackupStream: () => Readable,
|
createBackupStream: () => Readable,
|
||||||
backupType = BackupType.Ciphertext
|
{ backupType = BackupType.Ciphertext, onProgress }: ImportOptionsType = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
strictAssert(!this.isRunning, 'BackupService is already running');
|
strictAssert(!this.isRunning, 'BackupService is already running');
|
||||||
|
|
||||||
|
@ -236,8 +255,12 @@ export class BackupsService {
|
||||||
// First pass - don't decrypt, only verify mac
|
// First pass - don't decrypt, only verify mac
|
||||||
let hmac = createHmac(HashType.size256, macKey);
|
let hmac = createHmac(HashType.size256, macKey);
|
||||||
let theirMac: Uint8Array | undefined;
|
let theirMac: Uint8Array | undefined;
|
||||||
|
let totalBytes = 0;
|
||||||
|
|
||||||
const sink = new PassThrough();
|
const sink = new PassThrough();
|
||||||
|
sink.on('data', chunk => {
|
||||||
|
totalBytes += chunk.byteLength;
|
||||||
|
});
|
||||||
// Discard the data in the first pass
|
// Discard the data in the first pass
|
||||||
sink.resume();
|
sink.resume();
|
||||||
|
|
||||||
|
@ -249,6 +272,8 @@ export class BackupsService {
|
||||||
sink
|
sink
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onProgress?.(0, totalBytes);
|
||||||
|
|
||||||
strictAssert(theirMac != null, 'importBackup: Missing MAC');
|
strictAssert(theirMac != null, 'importBackup: Missing MAC');
|
||||||
strictAssert(
|
strictAssert(
|
||||||
constantTimeEqual(hmac.digest(), theirMac),
|
constantTimeEqual(hmac.digest(), theirMac),
|
||||||
|
@ -258,9 +283,19 @@ export class BackupsService {
|
||||||
// Second pass - decrypt (but still check the mac at the end)
|
// Second pass - decrypt (but still check the mac at the end)
|
||||||
hmac = createHmac(HashType.size256, macKey);
|
hmac = createHmac(HashType.size256, macKey);
|
||||||
|
|
||||||
|
const progressReporter = new PassThrough();
|
||||||
|
progressReporter.pause();
|
||||||
|
|
||||||
|
let currentBytes = 0;
|
||||||
|
progressReporter.on('data', chunk => {
|
||||||
|
currentBytes += chunk.byteLength;
|
||||||
|
onProgress?.(currentBytes, totalBytes);
|
||||||
|
});
|
||||||
|
|
||||||
await pipeline(
|
await pipeline(
|
||||||
createBackupStream(),
|
createBackupStream(),
|
||||||
getMacAndUpdateHmac(hmac, noop),
|
getMacAndUpdateHmac(hmac, noop),
|
||||||
|
progressReporter,
|
||||||
getIvAndDecipher(aesKey),
|
getIvAndDecipher(aesKey),
|
||||||
createGunzip(),
|
createGunzip(),
|
||||||
new DelimitedStream(),
|
new DelimitedStream(),
|
||||||
|
@ -343,7 +378,7 @@ export class BackupsService {
|
||||||
|
|
||||||
private async doDownload(
|
private async doDownload(
|
||||||
downloadPath: string,
|
downloadPath: string,
|
||||||
{ onProgress }: Omit<DownloadOptionsType, 'downloadOffset'>
|
{ onProgress }: Pick<DownloadOptionsType, 'onProgress'>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
@ -371,7 +406,13 @@ export class BackupsService {
|
||||||
|
|
||||||
const stream = await this.api.download({
|
const stream = await this.api.download({
|
||||||
downloadOffset,
|
downloadOffset,
|
||||||
onProgress,
|
onProgress: (currentBytes, totalBytes) => {
|
||||||
|
onProgress?.(
|
||||||
|
InstallScreenBackupStep.Download,
|
||||||
|
currentBytes,
|
||||||
|
totalBytes
|
||||||
|
);
|
||||||
|
},
|
||||||
abortSignal: controller.signal,
|
abortSignal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -395,7 +436,15 @@ export class BackupsService {
|
||||||
|
|
||||||
// Too late to cancel now
|
// Too late to cancel now
|
||||||
try {
|
try {
|
||||||
await this.importFromDisk(downloadPath);
|
await this.importFromDisk(downloadPath, {
|
||||||
|
onProgress: (currentBytes, totalBytes) => {
|
||||||
|
onProgress?.(
|
||||||
|
InstallScreenBackupStep.Process,
|
||||||
|
currentBytes,
|
||||||
|
totalBytes
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
await unlink(downloadPath);
|
await unlink(downloadPath);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import pTimeout, { TimeoutError } from 'p-timeout';
|
||||||
|
|
||||||
import type { StateType as RootStateType } from '../reducer';
|
import type { StateType as RootStateType } from '../reducer';
|
||||||
import {
|
import {
|
||||||
|
InstallScreenBackupStep,
|
||||||
InstallScreenStep,
|
InstallScreenStep,
|
||||||
InstallScreenError,
|
InstallScreenError,
|
||||||
InstallScreenQRCodeError,
|
InstallScreenQRCodeError,
|
||||||
|
@ -62,6 +63,7 @@ export type InstallerStateType = ReadonlyDeep<
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
step: InstallScreenStep.BackupImport;
|
step: InstallScreenStep.BackupImport;
|
||||||
|
backupStep: InstallScreenBackupStep;
|
||||||
currentBytes?: number;
|
currentBytes?: number;
|
||||||
totalBytes?: number;
|
totalBytes?: number;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
|
@ -124,6 +126,7 @@ type UpdateBackupImportProgressActionType = ReadonlyDeep<{
|
||||||
type: typeof UPDATE_BACKUP_IMPORT_PROGRESS;
|
type: typeof UPDATE_BACKUP_IMPORT_PROGRESS;
|
||||||
payload:
|
payload:
|
||||||
| {
|
| {
|
||||||
|
backupStep: InstallScreenBackupStep;
|
||||||
currentBytes: number;
|
currentBytes: number;
|
||||||
totalBytes: number;
|
totalBytes: number;
|
||||||
}
|
}
|
||||||
|
@ -560,6 +563,7 @@ export function reducer(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: InstallScreenStep.BackupImport,
|
step: InstallScreenStep.BackupImport,
|
||||||
|
backupStep: InstallScreenBackupStep.Download,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -581,6 +585,7 @@ export function reducer(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
backupStep: action.payload.backupStep,
|
||||||
currentBytes: action.payload.currentBytes,
|
currentBytes: action.payload.currentBytes,
|
||||||
totalBytes: action.payload.totalBytes,
|
totalBytes: action.payload.totalBytes,
|
||||||
};
|
};
|
||||||
|
|
|
@ -109,6 +109,7 @@ export const SmartInstallScreen = memo(function SmartInstallScreen() {
|
||||||
step: InstallScreenStep.BackupImport,
|
step: InstallScreenStep.BackupImport,
|
||||||
screenSpecificProps: {
|
screenSpecificProps: {
|
||||||
i18n,
|
i18n,
|
||||||
|
backupStep: installerState.backupStep,
|
||||||
currentBytes: installerState.currentBytes,
|
currentBytes: installerState.currentBytes,
|
||||||
totalBytes: installerState.totalBytes,
|
totalBytes: installerState.totalBytes,
|
||||||
hasError: installerState.hasError,
|
hasError: installerState.hasError,
|
||||||
|
|
|
@ -72,10 +72,9 @@ describe('backup/integration', () => {
|
||||||
it(basename(fullPath), async () => {
|
it(basename(fullPath), async () => {
|
||||||
const expectedBuffer = await readFile(fullPath);
|
const expectedBuffer = await readFile(fullPath);
|
||||||
|
|
||||||
await backupsService.importBackup(
|
await backupsService.importBackup(() => Readable.from([expectedBuffer]), {
|
||||||
() => Readable.from([expectedBuffer]),
|
backupType: BackupType.TestOnlyPlaintext,
|
||||||
BackupType.TestOnlyPlaintext
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const exported = await backupsService.exportBackupData(
|
const exported = await backupsService.exportBackupData(
|
||||||
BackupLevel.Media,
|
BackupLevel.Media,
|
||||||
|
|
|
@ -109,7 +109,6 @@ describe('AttachmentBackupManager/JobManager', function attachmentBackupManager(
|
||||||
keys: Bytes.fromBase64(LOCAL_ENCRYPTION_KEYS),
|
keys: Bytes.fromBase64(LOCAL_ENCRYPTION_KEYS),
|
||||||
needIncrementalMac: false,
|
needIncrementalMac: false,
|
||||||
sink: createWriteStream(absolutePath),
|
sink: createWriteStream(absolutePath),
|
||||||
getAbsoluteAttachmentPath,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -38,8 +38,6 @@ describe('utils/ensureAttachmentIsReencryptable', async () => {
|
||||||
plaintext: {
|
plaintext: {
|
||||||
absolutePath: plaintextFilePath,
|
absolutePath: plaintextFilePath,
|
||||||
},
|
},
|
||||||
getAbsoluteAttachmentPath:
|
|
||||||
window.Signal.Migrations.getAbsoluteAttachmentPath,
|
|
||||||
needIncrementalMac: false,
|
needIncrementalMac: false,
|
||||||
});
|
});
|
||||||
digest = encrypted.digest;
|
digest = encrypted.digest;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright 2023 Signal Messenger, LLC
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
import createDebug from 'debug';
|
import createDebug from 'debug';
|
||||||
import Long from 'long';
|
import Long from 'long';
|
||||||
import { Proto, StorageState } from '@signalapp/mock-server';
|
import { Proto, StorageState } from '@signalapp/mock-server';
|
||||||
|
@ -8,10 +10,16 @@ import { expect } from 'playwright/test';
|
||||||
|
|
||||||
import { generateStoryDistributionId } from '../../types/StoryDistributionId';
|
import { generateStoryDistributionId } from '../../types/StoryDistributionId';
|
||||||
import { MY_STORY_ID } from '../../types/Stories';
|
import { MY_STORY_ID } from '../../types/Stories';
|
||||||
|
import { IMAGE_JPEG } from '../../types/MIME';
|
||||||
import { uuidToBytes } from '../../util/uuidToBytes';
|
import { uuidToBytes } from '../../util/uuidToBytes';
|
||||||
import * as durations from '../../util/durations';
|
import * as durations from '../../util/durations';
|
||||||
import type { App } from '../playwright';
|
import type { App } from '../playwright';
|
||||||
import { Bootstrap } from '../bootstrap';
|
import { Bootstrap } from '../bootstrap';
|
||||||
|
import {
|
||||||
|
getMessageInTimelineByTimestamp,
|
||||||
|
sendTextMessage,
|
||||||
|
sendReaction,
|
||||||
|
} from '../helpers';
|
||||||
|
|
||||||
export const debug = createDebug('mock:test:backups');
|
export const debug = createDebug('mock:test:backups');
|
||||||
|
|
||||||
|
@ -19,6 +27,15 @@ const IdentifierType = Proto.ManifestRecord.Identifier.Type;
|
||||||
|
|
||||||
const DISTRIBUTION1 = generateStoryDistributionId();
|
const DISTRIBUTION1 = generateStoryDistributionId();
|
||||||
|
|
||||||
|
const CAT_PATH = join(
|
||||||
|
__dirname,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'fixtures',
|
||||||
|
'cat-screenshot.png'
|
||||||
|
);
|
||||||
|
|
||||||
describe('backups', function (this: Mocha.Suite) {
|
describe('backups', function (this: Mocha.Suite) {
|
||||||
this.timeout(100 * durations.MINUTE);
|
this.timeout(100 * durations.MINUTE);
|
||||||
|
|
||||||
|
@ -102,9 +119,10 @@ describe('backups', function (this: Mocha.Suite) {
|
||||||
const [friend, pinned] = contacts;
|
const [friend, pinned] = contacts;
|
||||||
|
|
||||||
{
|
{
|
||||||
debug('wait for storage service sync to finish');
|
|
||||||
const window = await app.getWindow();
|
const window = await app.getWindow();
|
||||||
|
|
||||||
|
debug('wait for storage service sync to finish');
|
||||||
|
|
||||||
const leftPane = window.locator('#LeftPane');
|
const leftPane = window.locator('#LeftPane');
|
||||||
const contact = leftPane.locator(
|
const contact = leftPane.locator(
|
||||||
`[data-testid="${pinned.device.aci}"] >> "${pinned.profileName}"`
|
`[data-testid="${pinned.device.aci}"] >> "${pinned.profileName}"`
|
||||||
|
@ -137,55 +155,85 @@ describe('backups', function (this: Mocha.Suite) {
|
||||||
await backButton.last().click();
|
await backButton.last().click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sends = new Array<Promise<void>>();
|
||||||
|
|
||||||
for (let i = 0; i < 5; i += 1) {
|
for (let i = 0; i < 5; i += 1) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
sends.push(
|
||||||
await server.send(
|
sendTextMessage({
|
||||||
desktop,
|
from: phone,
|
||||||
// eslint-disable-next-line no-await-in-loop
|
to: pinned,
|
||||||
await phone.encryptSyncSent(desktop, `to pinned ${i}`, {
|
text: `to pinned ${i}`,
|
||||||
|
desktop,
|
||||||
timestamp: bootstrap.getTimestamp(),
|
timestamp: bootstrap.getTimestamp(),
|
||||||
destinationServiceId: pinned.device.aci,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const theirTimestamp = bootstrap.getTimestamp();
|
const theirTimestamp = bootstrap.getTimestamp();
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
sends.push(
|
||||||
await friend.sendText(desktop, `msg ${i}`, {
|
sendTextMessage({
|
||||||
timestamp: theirTimestamp,
|
from: friend,
|
||||||
});
|
to: desktop,
|
||||||
|
text: `msg ${i}`,
|
||||||
|
desktop,
|
||||||
|
timestamp: theirTimestamp,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const ourTimestamp = bootstrap.getTimestamp();
|
const ourTimestamp = bootstrap.getTimestamp();
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
sends.push(
|
||||||
await server.send(
|
sendTextMessage({
|
||||||
desktop,
|
from: phone,
|
||||||
// eslint-disable-next-line no-await-in-loop
|
to: friend,
|
||||||
await phone.encryptSyncSent(desktop, `respond ${i}`, {
|
text: `respond ${i}`,
|
||||||
|
desktop,
|
||||||
timestamp: ourTimestamp,
|
timestamp: ourTimestamp,
|
||||||
destinationServiceId: friend.device.aci,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const reactionTimestamp = bootstrap.getTimestamp();
|
const reactionTimestamp = bootstrap.getTimestamp();
|
||||||
|
sends.push(
|
||||||
|
sendReaction({
|
||||||
|
from: friend,
|
||||||
|
to: desktop,
|
||||||
|
targetAuthor: desktop,
|
||||||
|
targetMessageTimestamp: ourTimestamp,
|
||||||
|
reactionTimestamp,
|
||||||
|
desktop,
|
||||||
|
emoji: '👍',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
const catTimestamp = bootstrap.getTimestamp();
|
||||||
await friend.sendRaw(
|
const plaintextCat = await readFile(CAT_PATH);
|
||||||
|
const ciphertextCat = await bootstrap.storeAttachmentOnCDN(
|
||||||
|
plaintextCat,
|
||||||
|
IMAGE_JPEG
|
||||||
|
);
|
||||||
|
sends.push(
|
||||||
|
pinned.sendRaw(
|
||||||
desktop,
|
desktop,
|
||||||
{
|
{
|
||||||
dataMessage: {
|
dataMessage: {
|
||||||
timestamp: Long.fromNumber(reactionTimestamp),
|
timestamp: Long.fromNumber(catTimestamp),
|
||||||
reaction: {
|
attachments: [ciphertextCat],
|
||||||
emoji: '👍',
|
|
||||||
targetAuthorAci: desktop.aci,
|
|
||||||
targetTimestamp: Long.fromNumber(ourTimestamp),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamp: reactionTimestamp,
|
timestamp: catTimestamp,
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(sends);
|
||||||
|
|
||||||
|
{
|
||||||
|
const window = await app.getWindow();
|
||||||
|
await getMessageInTimelineByTimestamp(window, catTimestamp)
|
||||||
|
.locator('img')
|
||||||
|
.waitFor();
|
||||||
}
|
}
|
||||||
|
|
||||||
await app.uploadBackup();
|
await app.uploadBackup();
|
||||||
|
@ -195,7 +243,7 @@ describe('backups', function (this: Mocha.Suite) {
|
||||||
async (window, snapshot) => {
|
async (window, snapshot) => {
|
||||||
const leftPane = window.locator('#LeftPane');
|
const leftPane = window.locator('#LeftPane');
|
||||||
const pinnedElem = leftPane.locator(
|
const pinnedElem = leftPane.locator(
|
||||||
`[data-testid="${pinned.toContact().aci}"] >> "to pinned 4"`
|
`[data-testid="${pinned.toContact().aci}"] >> "Photo"`
|
||||||
);
|
);
|
||||||
|
|
||||||
debug('Waiting for messages to pinned contact to come through');
|
debug('Waiting for messages to pinned contact to come through');
|
||||||
|
@ -246,6 +294,7 @@ describe('backups', function (this: Mocha.Suite) {
|
||||||
|
|
||||||
// Restart
|
// Restart
|
||||||
await bootstrap.eraseStorage();
|
await bootstrap.eraseStorage();
|
||||||
|
await server.removeAllCDNAttachments();
|
||||||
app = await bootstrap.link();
|
app = await bootstrap.link();
|
||||||
await app.waitForBackupImportComplete();
|
await app.waitForBackupImportComplete();
|
||||||
|
|
||||||
|
@ -253,6 +302,14 @@ describe('backups', function (this: Mocha.Suite) {
|
||||||
// app won't show contacts as "system"
|
// app won't show contacts as "system"
|
||||||
await app.waitForContactSync();
|
await app.waitForContactSync();
|
||||||
|
|
||||||
|
debug('Waiting for attachments to be downloaded');
|
||||||
|
{
|
||||||
|
const window = await app.getWindow();
|
||||||
|
await window
|
||||||
|
.locator('.BackupMediaDownloadProgress__button-close')
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
await comparator(app);
|
await comparator(app);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,14 +6,16 @@ import fs from 'fs/promises';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import path, { join } from 'path';
|
import path, { join } from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
import { PassThrough } from 'node:stream';
|
||||||
import createDebug from 'debug';
|
import createDebug from 'debug';
|
||||||
import pTimeout from 'p-timeout';
|
import pTimeout from 'p-timeout';
|
||||||
import normalizePath from 'normalize-path';
|
import normalizePath from 'normalize-path';
|
||||||
import pixelmatch from 'pixelmatch';
|
import pixelmatch from 'pixelmatch';
|
||||||
import { PNG } from 'pngjs';
|
import { PNG } from 'pngjs';
|
||||||
import type { Page } from 'playwright';
|
import type { Page } from 'playwright';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import type { Device, PrimaryDevice } from '@signalapp/mock-server';
|
import type { Device, PrimaryDevice, Proto } from '@signalapp/mock-server';
|
||||||
import {
|
import {
|
||||||
Server,
|
Server,
|
||||||
ServiceIdKind,
|
ServiceIdKind,
|
||||||
|
@ -23,9 +25,14 @@ import { MAX_READ_KEYS as MAX_STORAGE_READ_KEYS } from '../services/storageConst
|
||||||
import * as durations from '../util/durations';
|
import * as durations from '../util/durations';
|
||||||
import { drop } from '../util/drop';
|
import { drop } from '../util/drop';
|
||||||
import type { RendererConfigType } from '../types/RendererConfig';
|
import type { RendererConfigType } from '../types/RendererConfig';
|
||||||
|
import type { MIMEType } from '../types/MIME';
|
||||||
import { App } from './playwright';
|
import { App } from './playwright';
|
||||||
import { CONTACT_COUNT } from './benchmarks/fixtures';
|
import { CONTACT_COUNT } from './benchmarks/fixtures';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
|
import {
|
||||||
|
encryptAttachmentV2,
|
||||||
|
generateAttachmentKeys,
|
||||||
|
} from '../AttachmentCrypto';
|
||||||
|
|
||||||
export { App };
|
export { App };
|
||||||
|
|
||||||
|
@ -540,6 +547,38 @@ export class Bootstrap {
|
||||||
return join(this.storagePath, 'attachments.noindex', relativePath);
|
return join(this.storagePath, 'attachments.noindex', relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async storeAttachmentOnCDN(
|
||||||
|
data: Buffer,
|
||||||
|
contentType: MIMEType
|
||||||
|
): Promise<Proto.IAttachmentPointer> {
|
||||||
|
const cdnKey = uuid();
|
||||||
|
const keys = generateAttachmentKeys();
|
||||||
|
const cdnNumber = 3;
|
||||||
|
|
||||||
|
const passthrough = new PassThrough();
|
||||||
|
|
||||||
|
const [{ digest }] = await Promise.all([
|
||||||
|
encryptAttachmentV2({
|
||||||
|
keys,
|
||||||
|
plaintext: {
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
needIncrementalMac: false,
|
||||||
|
sink: passthrough,
|
||||||
|
}),
|
||||||
|
this.server.storeAttachmentOnCdn(cdnNumber, cdnKey, passthrough),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
size: data.byteLength,
|
||||||
|
contentType,
|
||||||
|
cdnKey,
|
||||||
|
cdnNumber,
|
||||||
|
key: keys,
|
||||||
|
digest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Getters
|
// Getters
|
||||||
//
|
//
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import createDebug from 'debug';
|
import createDebug from 'debug';
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { expect } from 'playwright/test';
|
import { expect } from 'playwright/test';
|
||||||
import { readFileSync } from 'fs';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { type PrimaryDevice, StorageState } from '@signalapp/mock-server';
|
import { type PrimaryDevice, StorageState } from '@signalapp/mock-server';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { App } from '../playwright';
|
import type { App } from '../playwright';
|
||||||
|
@ -17,10 +17,6 @@ import {
|
||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
import * as durations from '../../util/durations';
|
import * as durations from '../../util/durations';
|
||||||
import { strictAssert } from '../../util/assert';
|
import { strictAssert } from '../../util/assert';
|
||||||
import {
|
|
||||||
encryptAttachmentV2ToDisk,
|
|
||||||
generateAttachmentKeys,
|
|
||||||
} from '../../AttachmentCrypto';
|
|
||||||
import { toBase64 } from '../../Bytes';
|
import { toBase64 } from '../../Bytes';
|
||||||
import type { AttachmentWithNewReencryptionInfoType } from '../../types/Attachment';
|
import type { AttachmentWithNewReencryptionInfoType } from '../../types/Attachment';
|
||||||
import { IMAGE_JPEG } from '../../types/MIME';
|
import { IMAGE_JPEG } from '../../types/MIME';
|
||||||
|
@ -144,34 +140,13 @@ describe('attachments', function (this: Mocha.Suite) {
|
||||||
|
|
||||||
await page.getByTestId(pinned.device.aci).click();
|
await page.getByTestId(pinned.device.aci).click();
|
||||||
|
|
||||||
const plaintextCat = readFileSync(CAT_PATH);
|
const plaintextCat = await readFile(CAT_PATH);
|
||||||
|
const attachment = await bootstrap.storeAttachmentOnCDN(
|
||||||
const cdnKey = 'cdnKey';
|
// add non-zero byte to the end of the data; this will be considered padding
|
||||||
const keys = generateAttachmentKeys();
|
// when received since we will include the size of the un-appended data when
|
||||||
const cdnNumber = 3;
|
// sending
|
||||||
|
Buffer.concat([plaintextCat, Buffer.from([1])]),
|
||||||
const { digest: newDigest, path: ciphertextPath } =
|
IMAGE_JPEG
|
||||||
await encryptAttachmentV2ToDisk({
|
|
||||||
keys,
|
|
||||||
plaintext: {
|
|
||||||
// add non-zero byte to the end of the data; this will be considered padding
|
|
||||||
// when received since we will include the size of the un-appended data when
|
|
||||||
// sending
|
|
||||||
data: Buffer.concat([plaintextCat, Buffer.from([1])]),
|
|
||||||
},
|
|
||||||
getAbsoluteAttachmentPath: relativePath =>
|
|
||||||
bootstrap.getAbsoluteAttachmentPath(relativePath),
|
|
||||||
needIncrementalMac: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ciphertextCatWithNonZeroPadding = readFileSync(
|
|
||||||
bootstrap.getAbsoluteAttachmentPath(ciphertextPath)
|
|
||||||
);
|
|
||||||
|
|
||||||
await bootstrap.server.storeAttachmentOnCdn(
|
|
||||||
cdnNumber,
|
|
||||||
cdnKey,
|
|
||||||
ciphertextCatWithNonZeroPadding
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const incomingTimestamp = Date.now();
|
const incomingTimestamp = Date.now();
|
||||||
|
@ -182,12 +157,8 @@ describe('attachments', function (this: Mocha.Suite) {
|
||||||
text: 'Wait, that is MY cat! But now with weird padding!',
|
text: 'Wait, that is MY cat! But now with weird padding!',
|
||||||
attachments: [
|
attachments: [
|
||||||
{
|
{
|
||||||
|
...attachment,
|
||||||
size: plaintextCat.byteLength,
|
size: plaintextCat.byteLength,
|
||||||
contentType: IMAGE_JPEG,
|
|
||||||
cdnKey,
|
|
||||||
cdnNumber,
|
|
||||||
key: keys,
|
|
||||||
digest: newDigest,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timestamp: incomingTimestamp,
|
timestamp: incomingTimestamp,
|
||||||
|
@ -209,8 +180,14 @@ describe('attachments', function (this: Mocha.Suite) {
|
||||||
assert.exists(incomingAttachment?.reencryptionInfo);
|
assert.exists(incomingAttachment?.reencryptionInfo);
|
||||||
assert.exists(incomingAttachment?.reencryptionInfo.digest);
|
assert.exists(incomingAttachment?.reencryptionInfo.digest);
|
||||||
|
|
||||||
assert.strictEqual(incomingAttachment?.key, toBase64(keys));
|
assert.strictEqual(
|
||||||
assert.strictEqual(incomingAttachment?.digest, toBase64(newDigest));
|
incomingAttachment?.key,
|
||||||
|
toBase64(attachment.key ?? new Uint8Array(0))
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
incomingAttachment?.digest,
|
||||||
|
toBase64(attachment.digest ?? new Uint8Array(0))
|
||||||
|
);
|
||||||
assert.notEqual(
|
assert.notEqual(
|
||||||
incomingAttachment?.digest,
|
incomingAttachment?.digest,
|
||||||
incomingAttachment.reencryptionInfo.digest
|
incomingAttachment.reencryptionInfo.digest
|
||||||
|
|
|
@ -12,6 +12,11 @@ export enum InstallScreenStep {
|
||||||
BackupImport = 'BackupImport',
|
BackupImport = 'BackupImport',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum InstallScreenBackupStep {
|
||||||
|
Download = 'Download',
|
||||||
|
Process = 'Process',
|
||||||
|
}
|
||||||
|
|
||||||
export enum InstallScreenError {
|
export enum InstallScreenError {
|
||||||
TooManyDevices = 'TooManyDevices',
|
TooManyDevices = 'TooManyDevices',
|
||||||
TooOld = 'TooOld',
|
TooOld = 'TooOld',
|
||||||
|
|
|
@ -95,8 +95,6 @@ export async function attemptToReencryptToOriginalDigest(
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
needIncrementalMac: false,
|
needIncrementalMac: false,
|
||||||
getAbsoluteAttachmentPath:
|
|
||||||
window.Signal.Migrations.getAbsoluteAttachmentPath,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
strictAssert(attachment.size != null, 'Size must exist');
|
strictAssert(attachment.size != null, 'Size must exist');
|
||||||
|
@ -127,8 +125,6 @@ export async function attemptToReencryptToOriginalDigest(
|
||||||
digestToMatch: fromBase64(digest),
|
digestToMatch: fromBase64(digest),
|
||||||
},
|
},
|
||||||
needIncrementalMac: false,
|
needIncrementalMac: false,
|
||||||
getAbsoluteAttachmentPath:
|
|
||||||
window.Signal.Migrations.getAbsoluteAttachmentPath,
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -150,8 +146,6 @@ export async function generateNewEncryptionInfoForAttachment(
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
needIncrementalMac: false,
|
needIncrementalMac: false,
|
||||||
getAbsoluteAttachmentPath:
|
|
||||||
window.Signal.Migrations.getAbsoluteAttachmentPath,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const passthrough = new PassThrough();
|
const passthrough = new PassThrough();
|
||||||
|
@ -177,8 +171,6 @@ export async function generateNewEncryptionInfoForAttachment(
|
||||||
size: attachment.size,
|
size: attachment.size,
|
||||||
},
|
},
|
||||||
needIncrementalMac: false,
|
needIncrementalMac: false,
|
||||||
getAbsoluteAttachmentPath:
|
|
||||||
window.Signal.Migrations.getAbsoluteAttachmentPath,
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
// eslint-disable-next-line prefer-destructuring
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
|
Loading…
Reference in a new issue