mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
Fix attachment ciphertext size calculations for backup tier downloads
This commit is contained in:
@@ -52,6 +52,7 @@ import { missingCaseError } from './util/missingCaseError.js';
|
||||
import { getEnvironment, Environment } from './environment.js';
|
||||
import { isNotEmpty, toBase64, toHex } from './Bytes.js';
|
||||
import { decipherWithAesKey } from './util/decipherWithAesKey.js';
|
||||
import { MediaTier } from './types/AttachmentDownload.js';
|
||||
|
||||
const { ensureFile } = fsExtra;
|
||||
|
||||
@@ -198,7 +199,12 @@ export async function encryptAttachmentV2({
|
||||
);
|
||||
}
|
||||
chunkSizeChoice = isNumber(size)
|
||||
? inferChunkSize(getAttachmentCiphertextLength(size))
|
||||
? inferChunkSize(
|
||||
getAttachmentCiphertextSize({
|
||||
unpaddedPlaintextSize: size,
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
})
|
||||
)
|
||||
: undefined;
|
||||
incrementalDigestCreator =
|
||||
needIncrementalMac && chunkSizeChoice
|
||||
@@ -664,20 +670,38 @@ export function measureSize({
|
||||
return passthrough;
|
||||
}
|
||||
|
||||
export function getAttachmentCiphertextLength(plaintextLength: number): number {
|
||||
const paddedPlaintextSize = logPadSize(plaintextLength);
|
||||
export function getAttachmentCiphertextSize({
|
||||
unpaddedPlaintextSize,
|
||||
mediaTier,
|
||||
}: {
|
||||
unpaddedPlaintextSize: number;
|
||||
mediaTier: MediaTier;
|
||||
}): number {
|
||||
const paddedSize = logPadSize(unpaddedPlaintextSize);
|
||||
|
||||
switch (mediaTier) {
|
||||
case MediaTier.STANDARD:
|
||||
return getCiphertextSize(paddedSize);
|
||||
case MediaTier.BACKUP:
|
||||
// objects on backup tier are doubly encrypted!
|
||||
return getCiphertextSize(getCiphertextSize(paddedSize));
|
||||
default:
|
||||
throw missingCaseError(mediaTier);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCiphertextSize(paddedPlaintextSize: number): number {
|
||||
return (
|
||||
IV_LENGTH +
|
||||
getAesCbcCiphertextLength(paddedPlaintextSize) +
|
||||
getAesCbcCiphertextSize(paddedPlaintextSize) +
|
||||
ATTACHMENT_MAC_LENGTH
|
||||
);
|
||||
}
|
||||
|
||||
export function getAesCbcCiphertextLength(plaintextLength: number): number {
|
||||
export function getAesCbcCiphertextSize(plaintextSize: number): number {
|
||||
const AES_CBC_BLOCK_SIZE = 16;
|
||||
return (
|
||||
(1 + Math.floor(plaintextLength / AES_CBC_BLOCK_SIZE)) * AES_CBC_BLOCK_SIZE
|
||||
(1 + Math.floor(plaintextSize / AES_CBC_BLOCK_SIZE)) * AES_CBC_BLOCK_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,8 @@ import {
|
||||
} from '../services/backups/index.js';
|
||||
import {
|
||||
type EncryptedAttachmentV2,
|
||||
getAttachmentCiphertextLength,
|
||||
getAesCbcCiphertextLength,
|
||||
decryptAttachmentV2ToSink,
|
||||
getAttachmentCiphertextSize,
|
||||
} from '../AttachmentCrypto.js';
|
||||
import {
|
||||
getBackupMediaRootKey,
|
||||
@@ -66,6 +65,7 @@ import { findRetryAfterTimeFromError } from './helpers/findRetryAfterTimeFromErr
|
||||
import { BackupCredentialType } from '../types/backups.js';
|
||||
import { supportsIncrementalMac } from '../types/MIME.js';
|
||||
import type { MIMEType } from '../types/MIME.js';
|
||||
import { MediaTier } from '../types/AttachmentDownload.js';
|
||||
|
||||
const log = createLogger('AttachmentBackupManager');
|
||||
|
||||
@@ -582,7 +582,10 @@ async function copyToBackupTier({
|
||||
dependencies.backupMediaBatch,
|
||||
'backupMediaBatch must be intialized'
|
||||
);
|
||||
const ciphertextLength = getAttachmentCiphertextLength(size);
|
||||
const ciphertextSizeOnTransitTier = getAttachmentCiphertextSize({
|
||||
unpaddedPlaintextSize: size,
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
});
|
||||
|
||||
const { responses } = await dependencies.backupMediaBatch({
|
||||
headers: await dependencies.backupsService.credentials.getHeadersForToday(
|
||||
@@ -594,7 +597,7 @@ async function copyToBackupTier({
|
||||
cdn: cdnNumber,
|
||||
key: cdnKey,
|
||||
},
|
||||
objectLength: ciphertextLength,
|
||||
objectLength: ciphertextSizeOnTransitTier,
|
||||
mediaId,
|
||||
hmacKey: macKey,
|
||||
encryptionKey: aesKey,
|
||||
@@ -613,9 +616,17 @@ async function copyToBackupTier({
|
||||
}
|
||||
|
||||
// Update our local understanding of what's in the backup cdn
|
||||
const sizeOnBackupCdn = getAesCbcCiphertextLength(ciphertextLength);
|
||||
const ciphertextSizeOnBackupTier = getAttachmentCiphertextSize({
|
||||
unpaddedPlaintextSize: size,
|
||||
mediaTier: MediaTier.BACKUP,
|
||||
});
|
||||
|
||||
await DataWriter.saveBackupCdnObjectMetadata([
|
||||
{ mediaId, cdnNumber: response.cdn, sizeOnBackupCdn },
|
||||
{
|
||||
mediaId,
|
||||
cdnNumber: response.cdn,
|
||||
sizeOnBackupCdn: ciphertextSizeOnBackupTier,
|
||||
},
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type CoreAttachmentDownloadJobType,
|
||||
AttachmentDownloadUrgency,
|
||||
coreAttachmentDownloadJobSchema,
|
||||
MediaTier,
|
||||
} from '../types/AttachmentDownload.js';
|
||||
import {
|
||||
downloadAttachment as downloadAttachmentUtil,
|
||||
@@ -52,7 +53,7 @@ import { IMAGE_WEBP } from '../types/MIME.js';
|
||||
import { AttachmentDownloadSource } from '../sql/Interface.js';
|
||||
import { drop } from '../util/drop.js';
|
||||
import {
|
||||
getAttachmentCiphertextLength,
|
||||
getAttachmentCiphertextSize,
|
||||
type ReencryptedAttachmentV2,
|
||||
} from '../AttachmentCrypto.js';
|
||||
import { safeParsePartial } from '../util/schemas.js';
|
||||
@@ -319,7 +320,15 @@ export class AttachmentDownloadManager extends JobManager<CoreAttachmentDownload
|
||||
const parseResult = safeParsePartial(coreAttachmentDownloadJobSchema, {
|
||||
attachment,
|
||||
attachmentType,
|
||||
ciphertextSize: getAttachmentCiphertextLength(attachment.size),
|
||||
// ciphertextSize is used for backup media download progress accounting; it may not
|
||||
// exactly match what we end up downloading, and that's OK (e.g. we may fallback to
|
||||
// download from the transit tier, which would be a slightly smaller size)
|
||||
ciphertextSize: getAttachmentCiphertextSize({
|
||||
unpaddedPlaintextSize: attachment.size,
|
||||
mediaTier: AttachmentDownloadSource.BACKUP_IMPORT_WITH_MEDIA
|
||||
? MediaTier.BACKUP
|
||||
: MediaTier.STANDARD,
|
||||
}),
|
||||
contentType: attachment.contentType,
|
||||
attachmentSignature: getUndownloadedAttachmentSignature(attachment),
|
||||
isManualDownload,
|
||||
|
||||
@@ -44,8 +44,8 @@ import {
|
||||
_generateAttachmentIv,
|
||||
decryptAttachmentV2,
|
||||
encryptAttachmentV2ToDisk,
|
||||
getAesCbcCiphertextLength,
|
||||
getAttachmentCiphertextLength,
|
||||
getAesCbcCiphertextSize,
|
||||
getAttachmentCiphertextSize,
|
||||
splitKeys,
|
||||
generateAttachmentKeys,
|
||||
type DecryptedAttachmentV2,
|
||||
@@ -55,6 +55,7 @@ import type { AciString, PniString } from '../types/ServiceId.js';
|
||||
import { createTempDir, deleteTempDir } from '../updater/common.js';
|
||||
import { uuidToBytes, bytesToUuid } from '../util/uuidToBytes.js';
|
||||
import { getPath } from '../windows/main/attachments.js';
|
||||
import { MediaTier } from '../types/AttachmentDownload.js';
|
||||
|
||||
const { emptyDir } = fsExtra;
|
||||
|
||||
@@ -780,7 +781,10 @@ describe('Crypto', () => {
|
||||
|
||||
assert.strictEqual(
|
||||
encryptedAttachment.ciphertextSize,
|
||||
getAttachmentCiphertextLength(data.byteLength)
|
||||
getAttachmentCiphertextSize({
|
||||
unpaddedPlaintextSize: data.byteLength,
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
})
|
||||
);
|
||||
|
||||
if (overrideSize == null) {
|
||||
@@ -1193,7 +1197,7 @@ describe('Crypto', () => {
|
||||
}
|
||||
it('calculates cipherTextLength correctly', () => {
|
||||
for (let i = 0; i < 128; i += 1) {
|
||||
assert.strictEqual(getAesCbcCiphertextLength(i), encrypt(i).length);
|
||||
assert.strictEqual(getAesCbcCiphertextSize(i), encrypt(i).length);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import {
|
||||
type AttachmentDownloadJobType,
|
||||
AttachmentDownloadUrgency,
|
||||
MediaTier,
|
||||
} from '../../types/AttachmentDownload.js';
|
||||
import { DataReader, DataWriter } from '../../sql/Client.js';
|
||||
import { DAY, MINUTE, MONTH } from '../../util/durations/index.js';
|
||||
@@ -29,7 +30,7 @@ import type { downloadAttachment as downloadAttachmentUtil } from '../../util/do
|
||||
import { AttachmentDownloadSource } from '../../sql/Interface.js';
|
||||
import {
|
||||
generateAttachmentKeys,
|
||||
getAttachmentCiphertextLength,
|
||||
getAttachmentCiphertextSize,
|
||||
} from '../../AttachmentCrypto.js';
|
||||
import { MEBIBYTE } from '../../types/AttachmentSize.js';
|
||||
import { generateAci } from '../../types/ServiceId.js';
|
||||
@@ -58,7 +59,10 @@ function composeJob({
|
||||
attachmentType: 'attachment',
|
||||
attachmentSignature: `${digest}.${plaintextHash}`,
|
||||
size,
|
||||
ciphertextSize: getAttachmentCiphertextLength(size),
|
||||
ciphertextSize: getAttachmentCiphertextSize({
|
||||
unpaddedPlaintextSize: size,
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
}),
|
||||
contentType,
|
||||
active: false,
|
||||
attempts: 0,
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '../types/Attachment.js';
|
||||
import * as Bytes from '../Bytes.js';
|
||||
import {
|
||||
getAttachmentCiphertextLength,
|
||||
getAttachmentCiphertextSize,
|
||||
safeUnlink,
|
||||
splitKeys,
|
||||
type ReencryptedAttachmentV2,
|
||||
@@ -142,6 +142,7 @@ export async function downloadAttachment(
|
||||
let downloadResult: Awaited<ReturnType<typeof downloadToDisk>>;
|
||||
|
||||
let downloadPath =
|
||||
mediaTier === MediaTier.STANDARD &&
|
||||
options.variant === AttachmentVariant.Default
|
||||
? attachment.downloadPath
|
||||
: undefined;
|
||||
@@ -170,11 +171,13 @@ export async function downloadAttachment(
|
||||
}
|
||||
}
|
||||
|
||||
const expectedCiphertextSize = getAttachmentCiphertextSize({
|
||||
unpaddedPlaintextSize: size,
|
||||
mediaTier,
|
||||
});
|
||||
|
||||
// Start over if we go over the size
|
||||
if (
|
||||
downloadOffset >= getAttachmentCiphertextLength(size) &&
|
||||
absoluteDownloadPath
|
||||
) {
|
||||
if (downloadOffset >= expectedCiphertextSize && absoluteDownloadPath) {
|
||||
log.warn('went over, retrying');
|
||||
await safeUnlink(absoluteDownloadPath);
|
||||
downloadOffset = 0;
|
||||
@@ -211,7 +214,7 @@ export async function downloadAttachment(
|
||||
downloadPath,
|
||||
downloadStream,
|
||||
onSizeUpdate: options.onSizeUpdate,
|
||||
size,
|
||||
expectedCiphertextSize,
|
||||
});
|
||||
} else {
|
||||
strictAssert(mediaTier === MediaTier.BACKUP, 'backup media tier');
|
||||
@@ -256,12 +259,14 @@ export async function downloadAttachment(
|
||||
downloadPath,
|
||||
downloadOffset,
|
||||
onSizeUpdate: options.onSizeUpdate,
|
||||
size: getAttachmentCiphertextLength(
|
||||
expectedCiphertextSize:
|
||||
options.variant === AttachmentVariant.ThumbnailFromBackup
|
||||
? // be generous, accept downloads of up to twice what we expect for thumbnail
|
||||
MAX_BACKUP_THUMBNAIL_SIZE * 2
|
||||
: size
|
||||
),
|
||||
? getAttachmentCiphertextSize({
|
||||
// to be generous, we accept downloads of up to twice what we expect
|
||||
unpaddedPlaintextSize: MAX_BACKUP_THUMBNAIL_SIZE * 2,
|
||||
mediaTier: MediaTier.BACKUP,
|
||||
})
|
||||
: expectedCiphertextSize,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -345,13 +350,13 @@ async function downloadToDisk({
|
||||
downloadPath,
|
||||
downloadStream,
|
||||
onSizeUpdate,
|
||||
size,
|
||||
expectedCiphertextSize,
|
||||
}: {
|
||||
downloadOffset?: number;
|
||||
downloadPath?: string;
|
||||
downloadStream: Readable;
|
||||
onSizeUpdate: (totalBytes: number) => void;
|
||||
size: number;
|
||||
expectedCiphertextSize: number;
|
||||
}): Promise<{ absolutePath: string; downloadSize: number }> {
|
||||
const absoluteTargetPath = downloadPath
|
||||
? window.Signal.Migrations.getAbsoluteDownloadsPath(downloadPath)
|
||||
@@ -373,7 +378,7 @@ async function downloadToDisk({
|
||||
writeStream = createWriteStream(absoluteTargetPath);
|
||||
}
|
||||
|
||||
const targetSize = getAttachmentCiphertextLength(size) - downloadOffset;
|
||||
const targetSize = expectedCiphertextSize - downloadOffset;
|
||||
let downloadSize = 0;
|
||||
|
||||
try {
|
||||
@@ -428,7 +433,7 @@ function checkSize(expectedBytes: number) {
|
||||
}
|
||||
|
||||
if (totalBytes > expectedBytes) {
|
||||
log.warn(
|
||||
log.error(
|
||||
`checkSize: Received ${totalBytes} bytes, expected ${expectedBytes}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ import { handleVideoAttachment } from './handleVideoAttachment.js';
|
||||
import { isHeic, stringToMIMEType } from '../types/MIME.js';
|
||||
import { ToastType } from '../types/Toast.js';
|
||||
import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome.js';
|
||||
import { getAttachmentCiphertextLength } from '../AttachmentCrypto.js';
|
||||
import { getAttachmentCiphertextSize } from '../AttachmentCrypto.js';
|
||||
import { MediaTier } from '../types/AttachmentDownload.js';
|
||||
|
||||
const log = createLogger('processAttachment');
|
||||
|
||||
@@ -85,7 +86,10 @@ function isAttachmentSizeOkay(attachment: Readonly<AttachmentType>): boolean {
|
||||
const limitBytes =
|
||||
getMaximumOutgoingAttachmentSizeInKb(getRemoteConfigValue) * KIBIBYTE;
|
||||
|
||||
const paddedAndEncryptedSize = getAttachmentCiphertextLength(attachment.size);
|
||||
const paddedAndEncryptedSize = getAttachmentCiphertextSize({
|
||||
unpaddedPlaintextSize: attachment.size,
|
||||
mediaTier: MediaTier.STANDARD,
|
||||
});
|
||||
if (paddedAndEncryptedSize > limitBytes) {
|
||||
window.reduxActions.toast.showToast({
|
||||
toastType: ToastType.FileSize,
|
||||
|
||||
Reference in New Issue
Block a user