Signal-Desktop/sticker-creator/preload.js

274 lines
7.8 KiB
JavaScript
Raw Normal View History

2022-02-24 22:54:22 +00:00
// Copyright 2019-2022 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2019-12-17 20:25:57 +00:00
/* global window */
2021-10-27 17:54:16 +00:00
const { ipcRenderer: ipc } = require('electron');
2019-12-17 20:25:57 +00:00
const sharp = require('sharp');
const pify = require('pify');
const { readFile } = require('fs');
const config = require('url').parse(window.location.toString(), true).query;
const { noop, uniqBy } = require('lodash');
const pMap = require('p-map');
2021-08-18 20:08:14 +00:00
// It is important to call this as early as possible
2021-10-18 20:59:17 +00:00
const { SignalContext } = require('../ts/windows/context');
2021-08-18 20:08:14 +00:00
2022-02-24 02:07:42 +00:00
window.i18n = SignalContext.i18n;
2021-10-06 18:29:20 +00:00
const {
deriveStickerPackKey,
encryptAttachment,
getRandomBytes,
} = require('../ts/Crypto');
const Bytes = require('../ts/Bytes');
2021-07-02 19:21:24 +00:00
const { SignalService: Proto } = require('../ts/protobuf');
2021-10-18 20:59:17 +00:00
const { getEnvironment } = require('../ts/environment');
2021-08-18 20:08:14 +00:00
const { createSetting } = require('../ts/util/preload');
2019-12-17 20:25:57 +00:00
2020-09-28 18:40:26 +00:00
const STICKER_SIZE = 512;
const MIN_STICKER_DIMENSION = 10;
const MAX_STICKER_DIMENSION = STICKER_SIZE;
2022-02-24 22:54:22 +00:00
const MAX_STICKER_BYTE_LENGTH = 300 * 1024;
2020-09-28 18:40:26 +00:00
2019-12-17 20:25:57 +00:00
window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/';
window.getEnvironment = getEnvironment;
2019-12-17 20:25:57 +00:00
window.getVersion = () => config.version;
window.PQueue = require('p-queue').default;
window.Backbone = require('backbone');
2019-12-17 20:25:57 +00:00
window.localeMessages = ipc.sendSync('locale-data');
require('../ts/SignalProtocolStore');
2021-10-18 20:59:17 +00:00
SignalContext.log.info('sticker-creator starting up...');
2019-12-17 20:25:57 +00:00
const Signal = require('../js/modules/signal');
window.Signal = Signal.setup({});
window.textsecure = require('../ts/textsecure').default;
2019-12-17 20:25:57 +00:00
const { initialize: initializeWebAPI } = require('../ts/textsecure/WebAPI');
2020-09-28 18:40:26 +00:00
const {
getAnimatedPngDataIfExists,
} = require('../ts/util/getAnimatedPngDataIfExists');
2019-12-17 20:25:57 +00:00
const WebAPI = initializeWebAPI({
url: config.serverUrl,
storageUrl: config.storageUrl,
2021-11-09 16:53:37 +00:00
updatesUrl: config.updatesUrl,
2022-03-09 19:28:40 +00:00
directoryVersion: parseInt(config.directoryVersion, 10),
2020-09-04 01:25:19 +00:00
directoryUrl: config.directoryUrl,
directoryEnclaveId: config.directoryEnclaveId,
directoryTrustAnchor: config.directoryTrustAnchor,
2021-11-08 23:32:31 +00:00
directoryV2Url: config.directoryV2Url,
directoryV2PublicKey: config.directoryV2PublicKey,
2022-03-09 19:28:40 +00:00
directoryV2CodeHashes: (config.directoryV2CodeHashes || '').split(','),
cdnUrlObject: {
0: config.cdnUrl0,
2: config.cdnUrl2,
},
2019-12-17 20:25:57 +00:00
certificateAuthority: config.certificateAuthority,
contentProxyUrl: config.contentProxyUrl,
proxyUrl: config.proxyUrl,
version: config.version,
2019-12-17 20:25:57 +00:00
});
2020-09-28 18:40:26 +00:00
function processStickerError(message, i18nKey) {
const result = new Error(message);
result.errorMessageI18nKey = i18nKey;
return result;
}
window.processStickerImage = async path => {
const imgBuffer = await pify(readFile)(path);
const sharpImg = sharp(imgBuffer);
const meta = await sharpImg.metadata();
2020-09-28 18:40:26 +00:00
const { width, height } = meta;
if (!width || !height) {
throw processStickerError(
'Sticker height or width were falsy',
'StickerCreator--Toasts--errorProcessing'
);
}
let contentType;
let processedBuffer;
// [Sharp doesn't support APNG][0], so we do something simpler: validate the file size
// and dimensions without resizing, cropping, or converting. In a perfect world, we'd
// resize and convert any animated image (GIF, animated WebP) to APNG.
// [0]: https://github.com/lovell/sharp/issues/2375
const animatedPngDataIfExists = getAnimatedPngDataIfExists(imgBuffer);
if (animatedPngDataIfExists) {
2022-02-24 22:54:22 +00:00
if (imgBuffer.byteLength > MAX_STICKER_BYTE_LENGTH) {
2020-09-28 18:40:26 +00:00
throw processStickerError(
'Sticker file was too large',
'StickerCreator--Toasts--tooLarge'
);
}
if (width !== height) {
throw processStickerError(
'Sticker must be square',
'StickerCreator--Toasts--APNG--notSquare'
);
}
if (width > MAX_STICKER_DIMENSION) {
throw processStickerError(
'Sticker dimensions are too large',
'StickerCreator--Toasts--APNG--dimensionsTooLarge'
);
}
if (width < MIN_STICKER_DIMENSION) {
throw processStickerError(
'Sticker dimensions are too small',
'StickerCreator--Toasts--APNG--dimensionsTooSmall'
);
}
if (animatedPngDataIfExists.numPlays !== Infinity) {
throw processStickerError(
'Animated stickers must loop forever',
'StickerCreator--Toasts--mustLoopForever'
);
}
contentType = 'image/png';
processedBuffer = imgBuffer;
} else {
contentType = 'image/webp';
processedBuffer = await sharpImg
.resize({
width: STICKER_SIZE,
height: STICKER_SIZE,
fit: 'contain',
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.webp()
.toBuffer();
2022-02-24 22:54:22 +00:00
if (processedBuffer.byteLength > MAX_STICKER_BYTE_LENGTH) {
throw processStickerError(
'Sticker file was too large',
'StickerCreator--Toasts--tooLarge'
);
}
2020-09-28 18:40:26 +00:00
}
2019-12-17 20:25:57 +00:00
return {
path,
2020-09-28 18:40:26 +00:00
buffer: processedBuffer,
src: `data:${contentType};base64,${processedBuffer.toString('base64')}`,
meta,
2019-12-17 20:25:57 +00:00
};
};
window.encryptAndUpload = async (
manifest,
stickers,
cover,
onProgress = noop
) => {
const usernameItem = await window.Signal.Data.getItemById('uuid_id');
const oldUsernameItem = await window.Signal.Data.getItemById('number_id');
2019-12-17 20:25:57 +00:00
const passwordItem = await window.Signal.Data.getItemById('password');
if (!oldUsernameItem || !passwordItem) {
2021-11-11 22:43:05 +00:00
const { message } =
window.localeMessages['StickerCreator--Authentication--error'];
2019-12-17 20:25:57 +00:00
2021-10-27 17:54:16 +00:00
ipc.send('show-message-box', {
2019-12-17 20:25:57 +00:00
type: 'warning',
message,
});
throw new Error(message);
}
const { value: username } = usernameItem;
const { value: oldUsername } = oldUsernameItem;
2019-12-17 20:25:57 +00:00
const { value: password } = passwordItem;
2021-10-06 18:29:20 +00:00
const packKey = getRandomBytes(32);
2021-09-24 00:49:05 +00:00
const encryptionKey = deriveStickerPackKey(packKey);
2021-10-06 18:29:20 +00:00
const iv = getRandomBytes(16);
2019-12-17 20:25:57 +00:00
const server = WebAPI.connect({
username: username || oldUsername,
password,
useWebSocket: false,
});
2019-12-17 20:25:57 +00:00
2020-09-28 18:40:26 +00:00
const uniqueStickers = uniqBy(
[...stickers, { imageData: cover }],
'imageData'
);
2019-12-17 20:25:57 +00:00
2021-07-02 19:21:24 +00:00
const manifestProto = new Proto.StickerPack();
2019-12-17 20:25:57 +00:00
manifestProto.title = manifest.title;
manifestProto.author = manifest.author;
manifestProto.stickers = stickers.map(({ emoji }, id) => {
2021-07-02 19:21:24 +00:00
const s = new Proto.StickerPack.Sticker();
2019-12-17 20:25:57 +00:00
s.id = id;
s.emoji = emoji;
return s;
});
2021-07-02 19:21:24 +00:00
const coverSticker = new Proto.StickerPack.Sticker();
2019-12-17 20:25:57 +00:00
coverSticker.id =
uniqueStickers.length === stickers.length ? 0 : uniqueStickers.length - 1;
coverSticker.emoji = '';
manifestProto.cover = coverSticker;
const encryptedManifest = await encrypt(
2021-07-02 19:21:24 +00:00
Proto.StickerPack.encode(manifestProto).finish(),
2019-12-17 20:25:57 +00:00
encryptionKey,
iv
);
const encryptedStickers = await pMap(
uniqueStickers,
2021-05-25 19:08:08 +00:00
({ imageData }) => encrypt(imageData.buffer, encryptionKey, iv),
{
concurrency: 3,
timeout: 1000 * 60 * 2,
}
2019-12-17 20:25:57 +00:00
);
const packId = await server.putStickers(
encryptedManifest,
encryptedStickers,
onProgress
);
2021-10-06 18:29:20 +00:00
const hexKey = Bytes.toHex(packKey);
2019-12-17 20:25:57 +00:00
ipc.send('install-sticker-pack', packId, hexKey);
return { packId, key: hexKey };
};
async function encrypt(data, key, iv) {
2021-10-06 18:29:20 +00:00
const { ciphertext } = await encryptAttachment(data, key, iv);
2019-12-17 20:25:57 +00:00
return ciphertext;
}
2021-11-16 20:40:29 +00:00
const getThemeSetting = createSetting('themeSetting');
2019-12-17 20:25:57 +00:00
async function resolveTheme() {
2021-08-18 20:08:14 +00:00
const theme = (await getThemeSetting.getValue()) || 'system';
if (theme === 'system') {
2022-01-06 23:34:53 +00:00
return SignalContext.nativeThemeListener.getSystemTheme();
2019-12-17 20:25:57 +00:00
}
return theme;
}
async function applyTheme() {
window.document.body.classList.remove('dark-theme');
window.document.body.classList.remove('light-theme');
window.document.body.classList.add(`${await resolveTheme()}-theme`);
}
window.addEventListener('DOMContentLoaded', applyTheme);
2021-10-18 20:59:17 +00:00
SignalContext.nativeThemeListener.subscribe(() => applyTheme());
2021-10-18 20:59:17 +00:00
SignalContext.log.info('sticker-creator preload complete...');