mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-05 01:10:49 +00:00
424 lines
12 KiB
TypeScript
424 lines
12 KiB
TypeScript
// Copyright 2024 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
import { BackupLevel } from '@signalapp/libsignal-client/zkgroup.js';
|
|
|
|
import {
|
|
APPLICATION_OCTET_STREAM,
|
|
stringToMIMEType,
|
|
} from '../../../types/MIME.std.js';
|
|
import type { AttachmentType } from '../../../types/Attachment.std.js';
|
|
import { doesAttachmentExist } from '../../../util/migrations.preload.js';
|
|
import {
|
|
hasRequiredInformationForLocalBackup,
|
|
hasRequiredInformationForRemoteBackup,
|
|
hasRequiredInformationToDownloadFromTransitTier,
|
|
} from '../../../util/Attachment.std.js';
|
|
import { Backups, SignalService } from '../../../protobuf/index.std.js';
|
|
import * as Bytes from '../../../Bytes.std.js';
|
|
import {
|
|
getSafeLongFromTimestamp,
|
|
getTimestampFromLong,
|
|
} from '../../../util/timestampLongUtils.std.js';
|
|
import { strictAssert } from '../../../util/assert.std.js';
|
|
import type {
|
|
CoreAttachmentBackupJobType,
|
|
CoreAttachmentLocalBackupJobType,
|
|
} from '../../../types/AttachmentBackup.std.js';
|
|
import {
|
|
type GetBackupCdnInfoType,
|
|
getMediaIdFromMediaName,
|
|
getMediaName,
|
|
getMediaNameForAttachment,
|
|
type BackupCdnInfoType,
|
|
getLocalBackupFileNameForAttachment,
|
|
getLocalBackupFileName,
|
|
} from './mediaId.preload.js';
|
|
import { missingCaseError } from '../../../util/missingCaseError.std.js';
|
|
import { bytesToUuid } from '../../../util/uuidToBytes.std.js';
|
|
import { createName } from '../../../util/attachmentPath.node.js';
|
|
import { generateAttachmentKeys } from '../../../AttachmentCrypto.node.js';
|
|
import { getAttachmentLocalBackupPathFromSnapshotDir } from './localBackup.node.js';
|
|
import {
|
|
isValidAttachmentKey,
|
|
isValidDigest,
|
|
isValidPlaintextHash,
|
|
} from '../../../types/Crypto.std.js';
|
|
import type { BackupExportOptions, BackupImportOptions } from '../types.std.js';
|
|
import { isTestOrMockEnvironment } from '../../../environment.std.js';
|
|
|
|
export function convertFilePointerToAttachment(
|
|
filePointer: Backups.FilePointer,
|
|
options: BackupImportOptions,
|
|
testDependencies?: { _createName: (suffix?: string) => string }
|
|
): AttachmentType {
|
|
const {
|
|
contentType,
|
|
width,
|
|
height,
|
|
fileName,
|
|
caption,
|
|
blurHash,
|
|
incrementalMac,
|
|
incrementalMacChunkSize,
|
|
locatorInfo,
|
|
} = filePointer;
|
|
const doCreateName = testDependencies?._createName ?? createName;
|
|
|
|
const commonProps: AttachmentType = {
|
|
size: 0,
|
|
contentType: contentType
|
|
? stringToMIMEType(contentType)
|
|
: APPLICATION_OCTET_STREAM,
|
|
width: width ?? undefined,
|
|
height: height ?? undefined,
|
|
fileName: fileName ?? undefined,
|
|
caption: caption ?? undefined,
|
|
blurHash: blurHash ?? undefined,
|
|
incrementalMac: undefined,
|
|
chunkSize: undefined,
|
|
downloadPath: doCreateName(),
|
|
};
|
|
|
|
if (Bytes.isNotEmpty(incrementalMac) && incrementalMacChunkSize) {
|
|
commonProps.incrementalMac = Bytes.toBase64(incrementalMac);
|
|
commonProps.chunkSize = incrementalMacChunkSize;
|
|
}
|
|
|
|
if (!locatorInfo) {
|
|
return {
|
|
...commonProps,
|
|
error: true,
|
|
downloadPath: undefined,
|
|
};
|
|
}
|
|
|
|
const {
|
|
key,
|
|
localKey,
|
|
plaintextHash,
|
|
encryptedDigest,
|
|
size,
|
|
transitCdnKey,
|
|
transitCdnNumber,
|
|
transitTierUploadTimestamp,
|
|
mediaTierCdnNumber,
|
|
} = locatorInfo;
|
|
|
|
if (!Bytes.isNotEmpty(key)) {
|
|
return {
|
|
...commonProps,
|
|
error: true,
|
|
downloadPath: undefined,
|
|
};
|
|
}
|
|
|
|
let mediaName: string | undefined;
|
|
if (Bytes.isNotEmpty(plaintextHash) && Bytes.isNotEmpty(key)) {
|
|
mediaName =
|
|
getMediaName({
|
|
key,
|
|
plaintextHash,
|
|
}) ?? undefined;
|
|
}
|
|
|
|
let localBackupPath: string | undefined;
|
|
if (
|
|
options.type === 'local-encrypted' &&
|
|
Bytes.isNotEmpty(localKey) &&
|
|
Bytes.isNotEmpty(plaintextHash)
|
|
) {
|
|
const localMediaName = getLocalBackupFileName({ plaintextHash, localKey });
|
|
localBackupPath = getAttachmentLocalBackupPathFromSnapshotDir(
|
|
localMediaName,
|
|
options.localBackupSnapshotDir
|
|
);
|
|
}
|
|
|
|
return {
|
|
...commonProps,
|
|
key: Bytes.toBase64(key),
|
|
digest: Bytes.isNotEmpty(encryptedDigest)
|
|
? Bytes.toBase64(encryptedDigest)
|
|
: undefined,
|
|
size: size ?? 0,
|
|
cdnKey: transitCdnKey ?? undefined,
|
|
cdnNumber: transitCdnNumber ?? undefined,
|
|
uploadTimestamp: transitTierUploadTimestamp
|
|
? getTimestampFromLong(transitTierUploadTimestamp)
|
|
: undefined,
|
|
plaintextHash: Bytes.isNotEmpty(plaintextHash)
|
|
? Bytes.toHex(plaintextHash)
|
|
: undefined,
|
|
localBackupPath,
|
|
// TODO: DESKTOP-8883
|
|
localKey: Bytes.isNotEmpty(localKey) ? Bytes.toBase64(localKey) : undefined,
|
|
...(mediaName && mediaTierCdnNumber != null
|
|
? {
|
|
backupCdnNumber: mediaTierCdnNumber,
|
|
}
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
export function convertBackupMessageAttachmentToAttachment(
|
|
messageAttachment: Backups.IMessageAttachment,
|
|
options: BackupImportOptions
|
|
): AttachmentType | null {
|
|
const { clientUuid } = messageAttachment;
|
|
|
|
if (!messageAttachment.pointer) {
|
|
return null;
|
|
}
|
|
const result = {
|
|
...convertFilePointerToAttachment(messageAttachment.pointer, options),
|
|
clientUuid: clientUuid ? bytesToUuid(clientUuid) : undefined,
|
|
};
|
|
|
|
switch (messageAttachment.flag) {
|
|
case Backups.MessageAttachment.Flag.VOICE_MESSAGE:
|
|
result.flags = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE;
|
|
break;
|
|
case Backups.MessageAttachment.Flag.BORDERLESS:
|
|
result.flags = SignalService.AttachmentPointer.Flags.BORDERLESS;
|
|
break;
|
|
case Backups.MessageAttachment.Flag.GIF:
|
|
result.flags = SignalService.AttachmentPointer.Flags.GIF;
|
|
break;
|
|
case Backups.MessageAttachment.Flag.NONE:
|
|
case null:
|
|
case undefined:
|
|
result.flags = undefined;
|
|
break;
|
|
default:
|
|
throw missingCaseError(messageAttachment.flag);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export async function getFilePointerForAttachment({
|
|
attachment: rawAttachment,
|
|
getBackupCdnInfo,
|
|
backupOptions,
|
|
messageReceivedAt,
|
|
}: {
|
|
attachment: Readonly<AttachmentType>;
|
|
getBackupCdnInfo: GetBackupCdnInfoType;
|
|
backupOptions: BackupExportOptions;
|
|
messageReceivedAt: number;
|
|
}): Promise<{
|
|
filePointer: Backups.FilePointer;
|
|
backupJob?: CoreAttachmentBackupJobType | CoreAttachmentLocalBackupJobType;
|
|
}> {
|
|
const attachment = maybeFixupAttachment(rawAttachment);
|
|
|
|
const filePointer = new Backups.FilePointer({
|
|
contentType: attachment.contentType,
|
|
fileName: attachment.fileName,
|
|
width: attachment.width,
|
|
height: attachment.height,
|
|
caption: attachment.caption,
|
|
blurHash: attachment.blurHash,
|
|
});
|
|
|
|
// TODO: DESKTOP-9112
|
|
if (isTestOrMockEnvironment()) {
|
|
// Check for string type for resilience to invalid data in the database from internal
|
|
// testing
|
|
if (typeof attachment.incrementalMac === 'string' && attachment.chunkSize) {
|
|
filePointer.incrementalMac = Bytes.fromBase64(attachment.incrementalMac);
|
|
filePointer.incrementalMacChunkSize = attachment.chunkSize;
|
|
}
|
|
}
|
|
|
|
const isAttachmentOnDisk =
|
|
attachment.path != null && (await doesAttachmentExist(attachment.path));
|
|
|
|
const remoteMediaName = hasRequiredInformationForRemoteBackup(attachment)
|
|
? getMediaNameForAttachment(attachment)
|
|
: undefined;
|
|
|
|
const remoteMediaId = remoteMediaName
|
|
? getMediaIdFromMediaName(remoteMediaName)
|
|
: undefined;
|
|
|
|
const remoteBackupStatus: BackupCdnInfoType = remoteMediaId
|
|
? await getBackupCdnInfo(remoteMediaId.string)
|
|
: { isInBackupTier: false };
|
|
|
|
const isLocalBackup =
|
|
backupOptions.type === 'local-encrypted' ||
|
|
backupOptions.type === 'plaintext-export';
|
|
filePointer.locatorInfo = getLocatorInfoForAttachment({
|
|
attachment,
|
|
backupOptions,
|
|
isOnDisk: isAttachmentOnDisk,
|
|
backupTierInfo: remoteBackupStatus,
|
|
});
|
|
|
|
if (isLocalBackup) {
|
|
if (
|
|
isAttachmentOnDisk &&
|
|
hasRequiredInformationForLocalBackup(attachment)
|
|
) {
|
|
return {
|
|
filePointer,
|
|
backupJob: {
|
|
isPlaintextExport: backupOptions.type === 'plaintext-export',
|
|
mediaName: getLocalBackupFileNameForAttachment(attachment),
|
|
type: 'local',
|
|
data: {
|
|
contentType: attachment.contentType,
|
|
fileName: attachment.fileName,
|
|
localKey: attachment.localKey,
|
|
path: attachment.path,
|
|
size: attachment.size,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
return {
|
|
filePointer,
|
|
backupJob: undefined,
|
|
};
|
|
}
|
|
|
|
if (backupOptions.level !== BackupLevel.Paid) {
|
|
return { filePointer, backupJob: undefined };
|
|
}
|
|
|
|
if (remoteBackupStatus.isInBackupTier) {
|
|
return { filePointer, backupJob: undefined };
|
|
}
|
|
|
|
if (!isAttachmentOnDisk) {
|
|
return { filePointer, backupJob: undefined };
|
|
}
|
|
|
|
if (!remoteMediaName) {
|
|
return { filePointer, backupJob: undefined };
|
|
}
|
|
|
|
const { path, localKey, key, version } = attachment;
|
|
|
|
strictAssert(path, 'Path must exist for attachment on disk');
|
|
strictAssert(key, 'Key must exist for remote backupable attachment');
|
|
|
|
const { transitCdnKey, transitCdnNumber, transitTierUploadTimestamp } =
|
|
filePointer.locatorInfo;
|
|
|
|
return {
|
|
filePointer,
|
|
backupJob: {
|
|
mediaName: remoteMediaName,
|
|
receivedAt: messageReceivedAt,
|
|
type: 'standard',
|
|
data: {
|
|
path,
|
|
localKey,
|
|
version,
|
|
contentType: attachment.contentType,
|
|
keys: key,
|
|
size: attachment.size,
|
|
transitCdnInfo:
|
|
transitCdnKey && transitCdnNumber != null
|
|
? {
|
|
cdnKey: transitCdnKey,
|
|
cdnNumber: transitCdnNumber,
|
|
uploadTimestamp: transitTierUploadTimestamp?.toNumber(),
|
|
}
|
|
: undefined,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function maybeFixupAttachment(attachment: AttachmentType): AttachmentType {
|
|
// Fixup attachment which has plaintextHash but no key
|
|
if (
|
|
isValidPlaintextHash(attachment.plaintextHash) &&
|
|
!isValidAttachmentKey(attachment.key)
|
|
) {
|
|
const fixedUpAttachment = { ...attachment };
|
|
fixedUpAttachment.key = Bytes.toBase64(generateAttachmentKeys());
|
|
// Delete all info dependent on key
|
|
delete fixedUpAttachment.cdnKey;
|
|
delete fixedUpAttachment.cdnNumber;
|
|
delete fixedUpAttachment.uploadTimestamp;
|
|
delete fixedUpAttachment.digest;
|
|
delete fixedUpAttachment.backupCdnNumber;
|
|
|
|
strictAssert(
|
|
hasRequiredInformationForRemoteBackup(fixedUpAttachment),
|
|
'should be backupable with new key'
|
|
);
|
|
return fixedUpAttachment;
|
|
}
|
|
return attachment;
|
|
}
|
|
function getLocatorInfoForAttachment({
|
|
attachment,
|
|
backupOptions,
|
|
isOnDisk,
|
|
backupTierInfo,
|
|
}: {
|
|
attachment: AttachmentType;
|
|
backupOptions: BackupExportOptions;
|
|
isOnDisk: boolean;
|
|
backupTierInfo: BackupCdnInfoType;
|
|
}): Backups.FilePointer.LocatorInfo {
|
|
const locatorInfo = new Backups.FilePointer.LocatorInfo();
|
|
|
|
const isLocalBackup =
|
|
backupOptions.type === 'local-encrypted' ||
|
|
backupOptions.type === 'plaintext-export';
|
|
|
|
const shouldBeLocallyBackedUp =
|
|
isLocalBackup &&
|
|
isOnDisk &&
|
|
hasRequiredInformationForLocalBackup(attachment);
|
|
|
|
const isDownloadableFromTransitTier =
|
|
hasRequiredInformationToDownloadFromTransitTier(attachment);
|
|
|
|
if (
|
|
!shouldBeLocallyBackedUp &&
|
|
!isDownloadableFromTransitTier &&
|
|
!hasRequiredInformationForRemoteBackup(attachment)
|
|
) {
|
|
return locatorInfo;
|
|
}
|
|
|
|
locatorInfo.size = attachment.size;
|
|
|
|
if (isValidAttachmentKey(attachment.key)) {
|
|
locatorInfo.key = Bytes.fromBase64(attachment.key);
|
|
}
|
|
|
|
if (isValidPlaintextHash(attachment.plaintextHash)) {
|
|
locatorInfo.plaintextHash = Bytes.fromHex(attachment.plaintextHash);
|
|
} else if (isValidDigest(attachment.digest)) {
|
|
locatorInfo.encryptedDigest = Bytes.fromBase64(attachment.digest);
|
|
}
|
|
|
|
if (isDownloadableFromTransitTier) {
|
|
locatorInfo.transitCdnKey = attachment.cdnKey;
|
|
locatorInfo.transitCdnNumber = attachment.cdnNumber;
|
|
locatorInfo.transitTierUploadTimestamp = getSafeLongFromTimestamp(
|
|
attachment.uploadTimestamp
|
|
);
|
|
}
|
|
|
|
if (shouldBeLocallyBackedUp) {
|
|
locatorInfo.localKey = Bytes.fromBase64(attachment.localKey);
|
|
}
|
|
|
|
if (backupTierInfo.isInBackupTier && backupTierInfo.cdnNumber != null) {
|
|
locatorInfo.mediaTierCdnNumber = backupTierInfo.cdnNumber;
|
|
} else if (backupOptions.type === 'cross-client-integration-test') {
|
|
locatorInfo.mediaTierCdnNumber = attachment.backupCdnNumber;
|
|
}
|
|
|
|
return locatorInfo;
|
|
}
|