signal-desktop/ts/util/encryptConversationAttachments.ts
2024-07-22 11:16:33 -07:00

191 lines
5.3 KiB
TypeScript

// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import pMap from 'p-map';
import * as log from '../logging/log';
import { DataReader, DataWriter } from '../sql/Client';
import type { ConversationAttributesType } from '../model-types.d';
import { encryptLegacyAttachment } from './encryptLegacyAttachment';
import { AttachmentDisposition } from './getLocalAttachmentUrl';
import { isNotNil } from './isNotNil';
import { isSignalConversation } from './isSignalConversation';
import { getConversationIdForLogging } from './idForLogging';
const CONCURRENCY = 32;
type CleanupType = Array<() => Promise<void>>;
export async function encryptConversationAttachments(): Promise<void> {
const all = await DataReader.getAllConversations();
log.info(`encryptConversationAttachments: checking ${all.length}`);
const updated = (
await pMap(
all,
async convo => {
try {
return await encryptOne(convo);
} catch (error) {
log.error('encryptConversationAttachments: processing failed', error);
return undefined;
}
},
{ concurrency: CONCURRENCY }
)
).filter(isNotNil);
if (updated.length !== 0) {
log.info(`encryptConversationAttachments: updating ${updated.length}`);
await DataWriter.updateConversations(
updated.map(({ attributes }) => attributes)
);
const cleanup = updated.map(entry => entry.cleanup).flat();
log.info(`encryptConversationAttachments: cleaning up ${cleanup.length}`);
await pMap(
cleanup,
async fn => {
try {
await fn();
} catch (error) {
log.error('encryptConversationAttachments: cleanup failed', error);
}
},
{ concurrency: CONCURRENCY }
);
}
}
async function encryptOne(attributes: ConversationAttributesType): Promise<
| {
attributes: ConversationAttributesType;
cleanup: CleanupType;
}
| undefined
> {
if (isSignalConversation(attributes)) {
return undefined;
}
const logId = getConversationIdForLogging(attributes);
const result = { ...attributes };
const {
deleteAttachmentData,
deleteAvatar,
deleteDraftFile,
readAttachmentData,
readAvatarData,
readDraftData,
writeNewAttachmentData,
writeNewAvatarData,
writeNewDraftData,
} = window.Signal.Migrations;
const cleanup: CleanupType = [];
if (attributes.profileAvatar?.path) {
result.profileAvatar = await encryptLegacyAttachment(
attributes.profileAvatar,
{
logId: `${logId}.profileAvatar`,
readAttachmentData,
writeNewAttachmentData,
disposition: AttachmentDisposition.Attachment,
}
);
if (result.profileAvatar !== attributes.profileAvatar) {
const { path } = attributes.profileAvatar;
cleanup.push(() => deleteAttachmentData(path));
}
}
if (attributes.avatar?.path) {
result.avatar = await encryptLegacyAttachment(attributes.avatar, {
logId: `${logId}.avatar`,
readAttachmentData,
writeNewAttachmentData,
disposition: AttachmentDisposition.Attachment,
});
if (result.avatar !== attributes.avatar) {
const { path } = attributes.avatar;
cleanup.push(() => deleteAttachmentData(path));
}
}
if (attributes.avatars?.length) {
result.avatars = await Promise.all(
attributes.avatars.map(async (avatar, i) => {
if (avatar.version === 2 || !avatar.imagePath) {
return avatar;
}
const { path: imagePath, ...updated } = await encryptLegacyAttachment(
{
path: avatar.imagePath,
},
{
logId: `${logId}.avatars[${i}]`,
readAttachmentData: readAvatarData,
writeNewAttachmentData: writeNewAvatarData,
disposition: AttachmentDisposition.AvatarData,
}
);
const path = avatar.imagePath;
cleanup.push(() => deleteAvatar(path));
return {
...avatar,
...updated,
imagePath,
};
})
);
}
if (attributes.draftAttachments?.length) {
result.draftAttachments = await Promise.all(
attributes.draftAttachments.map(async (draft, i) => {
const updated = await encryptLegacyAttachment(draft, {
logId: `${logId}.draft[${i}]`,
readAttachmentData: readDraftData,
writeNewAttachmentData: writeNewDraftData,
disposition: AttachmentDisposition.Draft,
});
if (updated !== draft && draft.path) {
const { path } = draft;
cleanup.push(() => deleteDraftFile(path));
}
return updated;
})
);
}
if (attributes.draftEditMessage?.attachmentThumbnail) {
const path = attributes.draftEditMessage?.attachmentThumbnail;
// Just drop thumbnail reference. It is impossible to recover, and has
// minimal UI impact.
if (!path.startsWith('attachment://')) {
await window.storage.put('needOrphanedAttachmentCheck', true);
if (result.draftEditMessage) {
result.draftEditMessage.attachmentThumbnail = undefined;
}
// Just to trigger the save
cleanup.push(() => Promise.resolve());
}
}
// Unchanged
if (!cleanup.length) {
return undefined;
}
return { attributes: result, cleanup };
}