diff --git a/js/modules/backup.js b/js/modules/backup.js
deleted file mode 100644
index 35e75cb4d..000000000
--- a/js/modules/backup.js
+++ /dev/null
@@ -1,1235 +0,0 @@
-/* global Signal: false */
-/* global _: false */
-
-/* eslint-env browser */
-/* eslint-env node */
-
-/* eslint-disable no-param-reassign, guard-for-in */
-
-const fs = require('fs');
-const path = require('path');
-
-const { map, fromPairs } = require('lodash');
-const tar = require('tar');
-const tmp = require('tmp');
-const pify = require('pify');
-const rimraf = require('rimraf');
-const electronRemote = require('electron').remote;
-
-const crypto = require('./crypto');
-
-const { dialog, BrowserWindow } = electronRemote;
-
-module.exports = {
- getDirectoryForExport,
- exportToDirectory,
- importFromDirectory,
- // for testing
- _sanitizeFileName,
- _trimFileName,
- _getExportAttachmentFileName,
- _getAnonymousAttachmentFileName,
- _getConversationDirName,
- _getConversationLoggingName,
-};
-
-function stringify(object) {
- // eslint-disable-next-line no-restricted-syntax
- for (const key in object) {
- const val = object[key];
- if (val instanceof ArrayBuffer) {
- object[key] = {
- type: 'ArrayBuffer',
- encoding: 'base64',
- data: crypto.arrayBufferToBase64(val),
- };
- } else if (val instanceof Object) {
- object[key] = stringify(val);
- }
- }
- return object;
-}
-
-function unstringify(object) {
- if (!(object instanceof Object)) {
- throw new Error('unstringify expects an object');
- }
- // eslint-disable-next-line no-restricted-syntax
- for (const key in object) {
- const val = object[key];
- if (
- val &&
- val.type === 'ArrayBuffer' &&
- val.encoding === 'base64' &&
- typeof val.data === 'string'
- ) {
- object[key] = crypto.base64ToArrayBuffer(val.data);
- } else if (val instanceof Object) {
- object[key] = unstringify(object[key]);
- }
- }
- return object;
-}
-
-function createOutputStream(writer) {
- let wait = Promise.resolve();
- return {
- write(string) {
- // eslint-disable-next-line more/no-then
- wait = wait.then(
- () =>
- new Promise(resolve => {
- if (writer.write(string)) {
- resolve();
- return;
- }
-
- // If write() returns true, we don't need to wait for the drain event
- // https://nodejs.org/dist/latest-v7.x/docs/api/stream.html#stream_class_stream_writable
- writer.once('drain', resolve);
-
- // We don't register for the 'error' event here, only in close(). Otherwise,
- // we'll get "Possible EventEmitter memory leak detected" warnings.
- })
- );
- return wait;
- },
- async close() {
- await wait;
- return new Promise((resolve, reject) => {
- writer.once('finish', resolve);
- writer.once('error', reject);
- writer.end();
- });
- },
- };
-}
-
-async function exportConversationListToFile(parent) {
- const writer = await createFileAndWriter(parent, 'db.json');
- return exportConversationList(writer);
-}
-
-function writeArray(stream, array) {
- stream.write('[');
-
- for (let i = 0, max = array.length; i < max; i += 1) {
- if (i > 0) {
- stream.write(',');
- }
-
- const item = array[i];
-
- // We don't back up avatars; we'll get them in a future contact sync or profile fetch
- const cleaned = _.omit(item, ['avatar', 'profileAvatar']);
-
- stream.write(JSON.stringify(stringify(cleaned)));
- }
-
- stream.write(']');
-}
-
-function getPlainJS(collection) {
- return collection.map(model => model.attributes);
-}
-
-async function exportConversationList(fileWriter) {
- const stream = createOutputStream(fileWriter);
-
- stream.write('{');
-
- stream.write('"conversations": ');
- const conversations = await window.Signal.Data.getAllConversations();
- window.log.info(`Exporting ${conversations.length} conversations`);
- writeArray(stream, getPlainJS(conversations));
-
- stream.write('}');
- await stream.close();
-}
-
-async function importNonMessages(parent, options) {
- const file = 'db.json';
- const string = await readFileAsText(parent, file);
- return importFromJsonString(string, path.join(parent, file), options);
-}
-
-function eliminateClientConfigInBackup(data, targetPath) {
- const cleaned = _.pick(data, 'conversations');
- window.log.info('Writing configuration-free backup file back to disk');
- try {
- fs.writeFileSync(targetPath, JSON.stringify(cleaned));
- } catch (error) {
- window.log.error('Error writing cleaned-up backup to disk: ', error.stack);
- }
-}
-
-async function importConversationsFromJSON(conversations, options) {
- const { writeNewAttachmentData } = window.Signal.Migrations;
- const { conversationLookup } = options;
-
- let count = 0;
- let skipCount = 0;
-
- for (let i = 0, max = conversations.length; i < max; i += 1) {
- const toAdd = unstringify(conversations[i]);
- const haveConversationAlready = conversationLookup[getConversationKey(toAdd)];
-
- if (haveConversationAlready) {
- skipCount += 1;
- count += 1;
- // eslint-disable-next-line no-continue
- continue;
- }
-
- count += 1;
- // eslint-disable-next-line no-await-in-loop
- const migrated = await window.Signal.Types.Conversation.migrateConversation(toAdd, {
- writeNewAttachmentData,
- });
- // eslint-disable-next-line no-await-in-loop
- await window.Signal.Data.saveConversation(migrated);
- }
-
- window.log.info('Done importing conversations:', 'Total count:', count, 'Skipped:', skipCount);
-}
-
-async function importFromJsonString(jsonString, targetPath, options) {
- options = options || {};
- _.defaults(options, {
- forceLightImport: false,
- conversationLookup: {},
- });
-
- const result = {
- fullImport: true,
- };
-
- const importObject = JSON.parse(jsonString);
- delete importObject.debug;
-
- if (!importObject.sessions || options.forceLightImport) {
- result.fullImport = false;
-
- delete importObject.items;
- delete importObject.identityKeys;
- delete importObject.unprocessed;
-
- window.log.info('This is a light import; contacts, groups and messages only');
- }
-
- // We mutate the on-disk backup to prevent the user from importing client
- // configuration more than once - that causes lots of encryption errors.
- // This of course preserves the true data: conversations.
- eliminateClientConfigInBackup(importObject, targetPath);
-
- const storeNames = _.keys(importObject);
- window.log.info('Importing to these stores:', storeNames.join(', '));
-
- // Special-case conversations key here, going to SQLCipher
- const { conversations } = importObject;
- const remainingStoreNames = _.without(
- storeNames,
- 'conversations',
- 'unprocessed',
- 'groups' // in old data sets, but no longer included in database schema
- );
- await importConversationsFromJSON(conversations, options);
-
- const SAVE_FUNCTIONS = {
- items: window.Signal.Data.createOrUpdateItem,
- };
-
- await Promise.all(
- _.map(remainingStoreNames, async storeName => {
- const save = SAVE_FUNCTIONS[storeName];
- if (!_.isFunction(save)) {
- throw new Error(`importFromJsonString: Didn't have save function for store ${storeName}`);
- }
-
- window.log.info(`Importing items for store ${storeName}`);
- const toImport = importObject[storeName];
-
- if (!toImport || !toImport.length) {
- window.log.info(`No items in ${storeName} store`);
- return;
- }
-
- for (let i = 0, max = toImport.length; i < max; i += 1) {
- const toAdd = unstringify(toImport[i]);
- // eslint-disable-next-line no-await-in-loop
- await save(toAdd);
- }
-
- window.log.info('Done importing to store', storeName, 'Total count:', toImport.length);
- })
- );
-
- window.log.info('DB import complete');
- return result;
-}
-
-function createDirectory(parent, name) {
- return new Promise((resolve, reject) => {
- const sanitized = _sanitizeFileName(name);
- const targetDir = path.join(parent, sanitized);
- if (fs.existsSync(targetDir)) {
- resolve(targetDir);
- return;
- }
-
- fs.mkdir(targetDir, error => {
- if (error) {
- reject(error);
- return;
- }
-
- resolve(targetDir);
- });
- });
-}
-
-function createFileAndWriter(parent, name) {
- return new Promise(resolve => {
- const sanitized = _sanitizeFileName(name);
- const targetPath = path.join(parent, sanitized);
- const options = {
- flags: 'wx',
- };
- return resolve(fs.createWriteStream(targetPath, options));
- });
-}
-
-function readFileAsText(parent, name) {
- return new Promise((resolve, reject) => {
- const targetPath = path.join(parent, name);
- fs.readFile(targetPath, 'utf8', (error, string) => {
- if (error) {
- return reject(error);
- }
-
- return resolve(string);
- });
- });
-}
-
-// Buffer instances are also Uint8Array instances, but they might be a view
-// https://nodejs.org/docs/latest/api/buffer.html#buffer_buffers_and_typedarray
-const toArrayBuffer = nodeBuffer =>
- nodeBuffer.buffer.slice(nodeBuffer.byteOffset, nodeBuffer.byteOffset + nodeBuffer.byteLength);
-
-function readFileAsArrayBuffer(targetPath) {
- return new Promise((resolve, reject) => {
- // omitting the encoding to get a buffer back
- fs.readFile(targetPath, (error, buffer) => {
- if (error) {
- return reject(error);
- }
-
- return resolve(toArrayBuffer(buffer));
- });
- });
-}
-
-function _trimFileName(filename) {
- const components = filename.split('.');
- if (components.length <= 1) {
- return filename.slice(0, 30);
- }
-
- const extension = components[components.length - 1];
- const name = components.slice(0, components.length - 1);
- if (extension.length > 5) {
- return filename.slice(0, 30);
- }
-
- return `${name.join('.').slice(0, 24)}.${extension}`;
-}
-
-function _getExportAttachmentFileName(message, index, attachment) {
- if (attachment.fileName) {
- return _trimFileName(attachment.fileName);
- }
-
- let name = attachment.id;
-
- if (attachment.contentType) {
- const components = attachment.contentType.split('/');
- name += `.${components.length > 1 ? components[1] : attachment.contentType}`;
- }
-
- return name;
-}
-
-function _getAnonymousAttachmentFileName(message, index) {
- if (!index) {
- return message.id;
- }
- return `${message.id}-${index}`;
-}
-
-async function readEncryptedAttachment(dir, attachment, name, options) {
- options = options || {};
- const { key } = options;
-
- const sanitizedName = _sanitizeFileName(name);
- const targetPath = path.join(dir, sanitizedName);
-
- if (!fs.existsSync(targetPath)) {
- window.log.warn(`Warning: attachment ${sanitizedName} not found`);
- return;
- }
-
- const data = await readFileAsArrayBuffer(targetPath);
-
- const isEncrypted = !_.isUndefined(key);
-
- if (isEncrypted) {
- attachment.data = await crypto.decryptAttachment(key, attachment.path, data);
- } else {
- attachment.data = data;
- }
-}
-
-async function writeQuoteThumbnail(attachment, options) {
- if (!attachment || !attachment.thumbnail || !attachment.thumbnail.path) {
- return;
- }
-
- const { dir, message, index, key, newKey } = options;
- const filename = `${_getAnonymousAttachmentFileName(message, index)}-quote-thumbnail`;
- const target = path.join(dir, filename);
-
- await writeEncryptedAttachment(target, attachment.thumbnail.path, {
- key,
- newKey,
- filename,
- dir,
- });
-}
-
-async function writeQuoteThumbnails(quotedAttachments, options) {
- const { name } = options;
-
- try {
- await Promise.all(
- _.map(quotedAttachments, (attachment, index) =>
- writeQuoteThumbnail(
- attachment,
- Object.assign({}, options, {
- index,
- })
- )
- )
- );
- } catch (error) {
- window.log.error(
- 'writeThumbnails: error exporting conversation',
- name,
- ':',
- error && error.stack ? error.stack : error
- );
- throw error;
- }
-}
-
-async function writeAttachment(attachment, options) {
- if (!_.isString(attachment.path)) {
- throw new Error('writeAttachment: attachment.path was not a string!');
- }
-
- const { dir, message, index, key, newKey } = options;
- const filename = _getAnonymousAttachmentFileName(message, index);
- const target = path.join(dir, filename);
-
- await writeEncryptedAttachment(target, attachment.path, {
- key,
- newKey,
- filename,
- dir,
- });
-
- if (attachment.thumbnail && _.isString(attachment.thumbnail.path)) {
- const thumbnailName = `${_getAnonymousAttachmentFileName(message, index)}-thumbnail`;
- const thumbnailTarget = path.join(dir, thumbnailName);
- await writeEncryptedAttachment(thumbnailTarget, attachment.thumbnail.path, {
- key,
- newKey,
- filename: thumbnailName,
- dir,
- });
- }
-
- if (attachment.screenshot && _.isString(attachment.screenshot.path)) {
- const screenshotName = `${_getAnonymousAttachmentFileName(message, index)}-screenshot`;
- const screenshotTarget = path.join(dir, screenshotName);
- await writeEncryptedAttachment(screenshotTarget, attachment.screenshot.path, {
- key,
- newKey,
- filename: screenshotName,
- dir,
- });
- }
-}
-
-async function writeAttachments(attachments, options) {
- const { name } = options;
-
- const promises = _.map(attachments, (attachment, index) =>
- writeAttachment(
- attachment,
- Object.assign({}, options, {
- index,
- })
- )
- );
- try {
- await Promise.all(promises);
- } catch (error) {
- window.log.error(
- 'writeAttachments: error exporting conversation',
- name,
- ':',
- error && error.stack ? error.stack : error
- );
- throw error;
- }
-}
-
-async function writeAvatar(contact, options) {
- const { avatar } = contact || {};
- if (!avatar || !avatar.avatar || !avatar.avatar.path) {
- return;
- }
-
- const { dir, message, index, key, newKey } = options;
- const name = _getAnonymousAttachmentFileName(message, index);
- const filename = `${name}-contact-avatar`;
- const target = path.join(dir, filename);
-
- await writeEncryptedAttachment(target, avatar.avatar.path, {
- key,
- newKey,
- filename,
- dir,
- });
-}
-
-async function writeContactAvatars(contact, options) {
- const { name } = options;
-
- try {
- await Promise.all(
- _.map(contact, (item, index) =>
- writeAvatar(
- item,
- Object.assign({}, options, {
- index,
- })
- )
- )
- );
- } catch (error) {
- window.log.error(
- 'writeContactAvatars: error exporting conversation',
- name,
- ':',
- error && error.stack ? error.stack : error
- );
- throw error;
- }
-}
-
-async function writePreviewImage(preview, options) {
- const { image } = preview || {};
- if (!image || !image.path) {
- return;
- }
-
- const { dir, message, index, key, newKey } = options;
- const name = _getAnonymousAttachmentFileName(message, index);
- const filename = `${name}-preview`;
- const target = path.join(dir, filename);
-
- await writeEncryptedAttachment(target, image.path, {
- key,
- newKey,
- filename,
- dir,
- });
-}
-
-async function writePreviews(preview, options) {
- const { name } = options;
-
- try {
- await Promise.all(
- _.map(preview, (item, index) =>
- writePreviewImage(
- item,
- Object.assign({}, options, {
- index,
- })
- )
- )
- );
- } catch (error) {
- window.log.error(
- 'writePreviews: error exporting conversation',
- name,
- ':',
- error && error.stack ? error.stack : error
- );
- throw error;
- }
-}
-
-async function writeEncryptedAttachment(target, source, options = {}) {
- const { key, newKey, filename, dir } = options;
-
- if (fs.existsSync(target)) {
- if (newKey) {
- window.log.info(`Deleting attachment ${filename}; key has changed`);
- fs.unlinkSync(target);
- } else {
- window.log.info(`Skipping attachment ${filename}; already exists`);
- return;
- }
- }
-
- const { readAttachmentData } = Signal.Migrations;
- const data = await readAttachmentData(source);
- const ciphertext = await crypto.encryptAttachment(key, source, data);
-
- const writer = await createFileAndWriter(dir, filename);
- const stream = createOutputStream(writer);
- stream.write(Buffer.from(ciphertext));
- await stream.close();
-}
-
-function _sanitizeFileName(filename) {
- return filename.toString().replace(/[^a-z0-9.,+()'#\- ]/gi, '_');
-}
-
-async function exportConversation(conversation, options = {}) {
- const { name, dir, attachmentsDir, key, newKey } = options;
-
- if (!name) {
- throw new Error('Need a name!');
- }
- if (!dir) {
- throw new Error('Need a target directory!');
- }
- if (!attachmentsDir) {
- throw new Error('Need an attachments directory!');
- }
- if (!key) {
- throw new Error('Need a key to encrypt with!');
- }
-
- window.log.info('exporting conversation', name);
- const writer = await createFileAndWriter(dir, 'messages.json');
- const stream = createOutputStream(writer);
- stream.write('{"messages":[');
-
- const CHUNK_SIZE = 50;
- let count = 0;
- let complete = false;
-
- // We're looping from the most recent to the oldest
- let lastReceivedAt = Number.MAX_VALUE;
-
- while (!complete) {
- // eslint-disable-next-line no-await-in-loop
- const collection = await window.Signal.Data.getMessagesByConversation(conversation.id, {
- limit: CHUNK_SIZE,
- receivedAt: lastReceivedAt,
- });
- const messages = getPlainJS(collection);
-
- for (let i = 0, max = messages.length; i < max; i += 1) {
- const message = messages[i];
- if (count > 0) {
- stream.write(',');
- }
-
- count += 1;
-
- // skip message if it is disappearing, no matter the amount of time left
- if (message.expireTimer) {
- // eslint-disable-next-line no-continue
- continue;
- }
-
- const { attachments } = message;
- // eliminate attachment data from the JSON, since it will go to disk
- // Note: this is for legacy messages only, which stored attachment data in the db
- message.attachments = _.map(attachments, attachment => _.omit(attachment, ['data']));
- // completely drop any attachments in messages cached in error objects
- // TODO: move to lodash. Sadly, a number of the method signatures have changed!
- message.errors = _.map(message.errors, error => {
- if (error && error.args) {
- error.args = [];
- }
- if (error && error.stack) {
- error.stack = '';
- }
- return error;
- });
-
- const jsonString = JSON.stringify(stringify(message));
- stream.write(jsonString);
-
- if (attachments && attachments.length > 0) {
- // eslint-disable-next-line no-await-in-loop
- await writeAttachments(attachments, {
- dir: attachmentsDir,
- name,
- message,
- key,
- newKey,
- });
- }
-
- const quoteThumbnails = message.quote && message.quote.attachments;
- if (quoteThumbnails && quoteThumbnails.length > 0) {
- // eslint-disable-next-line no-await-in-loop
- await writeQuoteThumbnails(quoteThumbnails, {
- dir: attachmentsDir,
- name,
- message,
- key,
- newKey,
- });
- }
-
- const { contact } = message;
- if (contact && contact.length > 0) {
- // eslint-disable-next-line no-await-in-loop
- await writeContactAvatars(contact, {
- dir: attachmentsDir,
- name,
- message,
- key,
- newKey,
- });
- }
-
- const { preview } = message;
- if (preview && preview.length > 0) {
- // eslint-disable-next-line no-await-in-loop
- await writePreviews(preview, {
- dir: attachmentsDir,
- name,
- message,
- key,
- newKey,
- });
- }
- }
-
- const last = messages.length > 0 ? messages[messages.length - 1] : null;
- if (last) {
- lastReceivedAt = last.received_at;
- }
-
- if (messages.length < CHUNK_SIZE) {
- complete = true;
- }
- }
-
- stream.write(']}');
- await stream.close();
-}
-
-// Goals for directory names:
-// 1. Human-readable, for easy use and verification by user (names not just ids)
-// 2. Sorted just like the list of conversations in the left-pan (active_at)
-// 3. Disambiguated from other directories (active_at, truncated name, id)
-function _getConversationDirName(conversation) {
- const name = conversation.active_at || 'inactive';
- if (conversation.name) {
- return `${name} (${conversation.name.slice(0, 30)} ${conversation.id})`;
- }
- return `${name} (${conversation.id})`;
-}
-
-// Goals for logging names:
-// 1. Can be associated with files on disk
-// 2. Adequately disambiguated to enable debugging flow of execution
-// 3. Can be shared to the web without privacy concerns (there's no global redaction
-// logic for group ids, so we do it manually here)
-function _getConversationLoggingName(conversation) {
- let name = conversation.active_at || 'inactive';
- if (conversation.type === 'private') {
- name += ` (${conversation.id})`;
- } else {
- name += ` ([REDACTED_GROUP]${conversation.id.slice(-3)})`;
- }
- return name;
-}
-
-async function exportConversations(options) {
- options = options || {};
- const { messagesDir, attachmentsDir, key, newKey } = options;
-
- if (!messagesDir) {
- throw new Error('Need a messages directory!');
- }
- if (!attachmentsDir) {
- throw new Error('Need an attachments directory!');
- }
-
- const collection = await window.Signal.Data.getAllConversations();
- const conversations = collection.models;
-
- for (let i = 0, max = conversations.length; i < max; i += 1) {
- const conversation = conversations[i];
- const dirName = _getConversationDirName(conversation);
- const name = _getConversationLoggingName(conversation);
-
- // eslint-disable-next-line no-await-in-loop
- const dir = await createDirectory(messagesDir, dirName);
- // eslint-disable-next-line no-await-in-loop
- await exportConversation(conversation, {
- name,
- dir,
- attachmentsDir,
- key,
- newKey,
- });
- }
-
- window.log.info('Done exporting conversations!');
-}
-
-function getDirectory(options = {}) {
- return new Promise((resolve, reject) => {
- const browserWindow = BrowserWindow.getFocusedWindow();
- const dialogOptions = {
- title: options.title,
- properties: ['openDirectory'],
- buttonLabel: options.buttonLabel,
- };
-
- dialog.showOpenDialog(browserWindow, dialogOptions, directory => {
- if (!directory || !directory[0]) {
- const error = new Error('Error choosing directory');
- error.name = 'ChooseError';
- return reject(error);
- }
-
- return resolve(directory[0]);
- });
- });
-}
-
-function getDirContents(dir) {
- return new Promise((resolve, reject) => {
- fs.readdir(dir, (err, files) => {
- if (err) {
- reject(err);
- return;
- }
-
- files = _.map(files, file => path.join(dir, file));
-
- resolve(files);
- });
- });
-}
-
-async function loadAttachments(dir, getName, options) {
- options = options || {};
- const { message } = options;
-
- await Promise.all(
- _.map(message.attachments, async (attachment, index) => {
- const name = getName(message, index, attachment);
-
- await readEncryptedAttachment(dir, attachment, name, options);
-
- if (attachment.thumbnail && _.isString(attachment.thumbnail.path)) {
- const thumbnailName = `${name}-thumbnail`;
- await readEncryptedAttachment(dir, attachment.thumbnail, thumbnailName, options);
- }
-
- if (attachment.screenshot && _.isString(attachment.screenshot.path)) {
- const screenshotName = `${name}-screenshot`;
- await readEncryptedAttachment(dir, attachment.screenshot, screenshotName, options);
- }
- })
- );
-
- const quoteAttachments = message.quote && message.quote.attachments;
- await Promise.all(
- _.map(quoteAttachments, (attachment, index) => {
- const thumbnail = attachment && attachment.thumbnail;
- if (!thumbnail) {
- return null;
- }
-
- const name = `${getName(message, index)}-quote-thumbnail`;
- return readEncryptedAttachment(dir, thumbnail, name, options);
- })
- );
-
- const { contact } = message;
- await Promise.all(
- _.map(contact, (item, index) => {
- const avatar = item && item.avatar && item.avatar.avatar;
- if (!avatar) {
- return null;
- }
-
- const name = `${getName(message, index)}-contact-avatar`;
- return readEncryptedAttachment(dir, avatar, name, options);
- })
- );
-
- const { preview } = message;
- await Promise.all(
- _.map(preview, (item, index) => {
- const image = item && item.image;
- if (!image) {
- return null;
- }
-
- const name = `${getName(message, index)}-preview`;
- return readEncryptedAttachment(dir, image, name, options);
- })
- );
-}
-
-function saveMessage(message) {
- return saveAllMessages([message]);
-}
-
-async function saveAllMessages(rawMessages) {
- if (rawMessages.length === 0) {
- return;
- }
-
- try {
- const { writeMessageAttachments, upgradeMessageSchema } = Signal.Migrations;
- const importAndUpgrade = async message =>
- upgradeMessageSchema(await writeMessageAttachments(message));
-
- const messages = await Promise.all(rawMessages.map(importAndUpgrade));
-
- const { conversationId } = messages[0];
-
- await window.Signal.Data.saveMessages(messages);
-
- window.log.info(
- 'Saved',
- messages.length,
- 'messages for conversation',
- // Don't know if group or private conversation, so we blindly redact
- `[REDACTED]${conversationId.slice(-3)}`
- );
- } catch (error) {
- window.log.error('saveAllMessages error', error && error.message ? error.message : error);
- }
-}
-
-// To reduce the memory impact of attachments, we make individual saves to the
-// database for every message with an attachment. We load the attachment for a
-// message, save it, and only then do we move on to the next message. Thus, every
-// message with attachments needs to be removed from our overall message save with the
-// filter() call.
-async function importConversation(dir, options) {
- options = options || {};
- _.defaults(options, { messageLookup: {} });
-
- const { messageLookup, attachmentsDir, key } = options;
-
- let conversationId = 'unknown';
- let total = 0;
- let skipped = 0;
- let contents;
-
- try {
- contents = await readFileAsText(dir, 'messages.json');
- } catch (error) {
- window.log.error(`Warning: could not access messages.json in directory: ${dir}`);
- }
-
- let promiseChain = Promise.resolve();
-
- const json = JSON.parse(contents);
- if (json.messages && json.messages.length) {
- conversationId = `[REDACTED]${(json.messages[0].conversationId || '').slice(-3)}`;
- }
- total = json.messages.length;
-
- const messages = _.filter(json.messages, message => {
- message = unstringify(message);
-
- if (messageLookup[getMessageKey(message)]) {
- skipped += 1;
- return false;
- }
-
- const hasAttachments = message.attachments && message.attachments.length;
- const hasQuotedAttachments =
- message.quote && message.quote.attachments && message.quote.attachments.length > 0;
- const hasContacts = message.contact && message.contact.length;
- const hasPreviews = message.preview && message.preview.length;
-
- if (hasAttachments || hasQuotedAttachments || hasContacts || hasPreviews) {
- const importMessage = async () => {
- const getName = attachmentsDir
- ? _getAnonymousAttachmentFileName
- : _getExportAttachmentFileName;
- const parentDir = attachmentsDir || path.join(dir, message.received_at.toString());
-
- await loadAttachments(parentDir, getName, {
- message,
- key,
- });
- return saveMessage(message);
- };
-
- // eslint-disable-next-line more/no-then
- promiseChain = promiseChain.then(importMessage);
-
- return false;
- }
-
- return true;
- });
-
- await saveAllMessages(messages);
-
- await promiseChain;
- window.log.info(
- 'Finished importing conversation',
- conversationId,
- 'Total:',
- total,
- 'Skipped:',
- skipped
- );
-}
-
-async function importConversations(dir, options) {
- const contents = await getDirContents(dir);
- let promiseChain = Promise.resolve();
-
- _.forEach(contents, conversationDir => {
- if (!fs.statSync(conversationDir).isDirectory()) {
- return;
- }
-
- const loadConversation = () => importConversation(conversationDir, options);
-
- // eslint-disable-next-line more/no-then
- promiseChain = promiseChain.then(loadConversation);
- });
-
- return promiseChain;
-}
-
-function getMessageKey(message) {
- const ourNumber = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache();
- const source = message.source || ourNumber;
- if (source === ourNumber) {
- return `${source} ${message.timestamp}`;
- }
-
- const sourceDevice = message.sourceDevice || 1;
- return `${source}.${sourceDevice} ${message.timestamp}`;
-}
-async function loadMessagesLookup() {
- const array = await window.Signal.Data.getAllMessageIds();
- return fromPairs(map(array, item => [getMessageKey(item), true]));
-}
-
-function getConversationKey(conversation) {
- return conversation.id;
-}
-async function loadConversationLookup() {
- const array = await window.Signal.Data.getAllConversationIds();
- return fromPairs(map(array, item => [getConversationKey(item), true]));
-}
-
-function getDirectoryForExport() {
- return getDirectory();
-}
-
-async function compressArchive(file, targetDir) {
- const items = fs.readdirSync(targetDir);
- return tar.c(
- {
- gzip: true,
- file,
- cwd: targetDir,
- },
- items
- );
-}
-
-async function decompressArchive(file, targetDir) {
- return tar.x({
- file,
- cwd: targetDir,
- });
-}
-
-function writeFile(targetPath, contents) {
- return pify(fs.writeFile)(targetPath, contents);
-}
-
-// prettier-ignore
-const UNIQUE_ID = new Uint8Array([
- 1, 3, 4, 5, 6, 7, 8, 11,
- 23, 34, 1, 34, 3, 5, 45, 45,
- 1, 3, 4, 5, 6, 7, 8, 11,
- 23, 34, 1, 34, 3, 5, 45, 45,
-]);
-async function encryptFile(sourcePath, targetPath, options) {
- options = options || {};
-
- const { key } = options;
- if (!key) {
- throw new Error('Need key to do encryption!');
- }
-
- const plaintext = await readFileAsArrayBuffer(sourcePath);
- const ciphertext = await crypto.encryptFile(key, UNIQUE_ID, plaintext);
- return writeFile(targetPath, Buffer.from(ciphertext));
-}
-
-async function decryptFile(sourcePath, targetPath, options) {
- options = options || {};
-
- const { key } = options;
- if (!key) {
- throw new Error('Need key to do encryption!');
- }
-
- const ciphertext = await readFileAsArrayBuffer(sourcePath);
- const plaintext = await crypto.decryptFile(key, UNIQUE_ID, ciphertext);
- return writeFile(targetPath, Buffer.from(plaintext));
-}
-
-function createTempDir() {
- return pify(tmp.dir)();
-}
-
-function deleteAll(pattern) {
- return pify(rimraf)(pattern);
-}
-
-const ARCHIVE_NAME = 'messages.tar.gz';
-
-async function exportToDirectory(directory, options) {
- const env = window.getEnvironment();
- if (env !== 'test') {
- throw new Error('export is only supported in test mode');
- }
-
- options = options || {};
-
- if (!options.key) {
- throw new Error('Encrypted backup requires a key to encrypt with!');
- }
-
- let stagingDir;
- let encryptionDir;
- try {
- stagingDir = await createTempDir();
- encryptionDir = await createTempDir();
-
- const attachmentsDir = await createDirectory(directory, 'attachments');
-
- await exportConversationListToFile(stagingDir);
- await exportConversations(
- Object.assign({}, options, {
- messagesDir: stagingDir,
- attachmentsDir,
- })
- );
-
- const archivePath = path.join(directory, ARCHIVE_NAME);
- await compressArchive(archivePath, stagingDir);
- await encryptFile(archivePath, path.join(directory, ARCHIVE_NAME), options);
-
- window.log.info('done backing up!');
- return directory;
- } catch (error) {
- window.log.error('The backup went wrong!', error && error.stack ? error.stack : error);
- throw error;
- } finally {
- if (stagingDir) {
- await deleteAll(stagingDir);
- }
- if (encryptionDir) {
- await deleteAll(encryptionDir);
- }
- }
-}
-
-async function importFromDirectory(directory, options) {
- options = options || {};
-
- try {
- const lookups = await Promise.all([loadMessagesLookup(), loadConversationLookup()]);
- const [messageLookup, conversationLookup] = lookups;
- options = Object.assign({}, options, {
- messageLookup,
- conversationLookup,
- });
-
- const archivePath = path.join(directory, ARCHIVE_NAME);
- if (fs.existsSync(archivePath)) {
- const env = window.getEnvironment();
- if (env !== 'test') {
- throw new Error('import is only supported in test mode');
- }
-
- // we're in the world of an encrypted, zipped backup
- if (!options.key) {
- throw new Error('Importing an encrypted backup; decryption key is required!');
- }
-
- let stagingDir;
- let decryptionDir;
- try {
- stagingDir = await createTempDir();
- decryptionDir = await createTempDir();
-
- const attachmentsDir = path.join(directory, 'attachments');
-
- const decryptedArchivePath = path.join(decryptionDir, ARCHIVE_NAME);
- await decryptFile(archivePath, decryptedArchivePath, options);
- await decompressArchive(decryptedArchivePath, stagingDir);
-
- options = Object.assign({}, options, {
- attachmentsDir,
- });
- const result = await importNonMessages(stagingDir, options);
- await importConversations(stagingDir, Object.assign({}, options));
-
- window.log.info('Done importing from backup!');
- return result;
- } finally {
- if (stagingDir) {
- await deleteAll(stagingDir);
- }
- if (decryptionDir) {
- await deleteAll(decryptionDir);
- }
- }
- }
-
- const result = await importNonMessages(directory, options);
- await importConversations(directory, options);
-
- window.log.info('Done importing!');
- return result;
- } catch (error) {
- window.log.error('The import went wrong!', error && error.stack ? error.stack : error);
- throw error;
- }
-}
diff --git a/js/modules/crypto.js b/js/modules/crypto.js
index 979e0d181..9b470947d 100644
--- a/js/modules/crypto.js
+++ b/js/modules/crypto.js
@@ -5,93 +5,28 @@
module.exports = {
arrayBufferToBase64,
- base64ToArrayBuffer,
bytesFromString,
concatenateBytes,
constantTimeEqual,
- decryptAesCtr,
- decryptAttachment,
- decryptFile,
decryptSymmetric,
deriveAccessKey,
encryptAesCtr,
- encryptAttachment,
- encryptFile,
encryptSymmetric,
- fromEncodedBinaryToArrayBuffer,
getRandomBytes,
- getViewOfArrayBuffer,
getZeroes,
- highBitsToInt,
hmacSha256,
- intsToByteHighAndLow,
- splitBytes,
- stringFromBytes,
- trimBytes,
};
function arrayBufferToBase64(arrayBuffer) {
return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
}
-function base64ToArrayBuffer(base64string) {
- return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();
-}
-
-function fromEncodedBinaryToArrayBuffer(key) {
- return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();
-}
function bytesFromString(string) {
return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();
}
-function stringFromBytes(buffer) {
- return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');
-}
// High-level Operations
-// Path structure: 'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa'
-function getAttachmentLabel(path) {
- const filename = path.slice(3);
- return base64ToArrayBuffer(filename);
-}
-
-const PUB_KEY_LENGTH = 32;
-async function encryptAttachment(staticPublicKey, path, plaintext) {
- const uniqueId = getAttachmentLabel(path);
- return encryptFile(staticPublicKey, uniqueId, plaintext);
-}
-
-async function decryptAttachment(staticPrivateKey, path, data) {
- const uniqueId = getAttachmentLabel(path);
- return decryptFile(staticPrivateKey, uniqueId, data);
-}
-
-async function encryptFile(staticPublicKey, uniqueId, plaintext) {
- const ephemeralKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
- const agreement = await libsignal.Curve.async.calculateAgreement(
- staticPublicKey,
- ephemeralKeyPair.privKey
- );
- const key = await hmacSha256(agreement, uniqueId);
-
- const prefix = ephemeralKeyPair.pubKey.slice(1);
- return concatenateBytes(prefix, await encryptSymmetric(key, plaintext));
-}
-
-async function decryptFile(staticPrivateKey, uniqueId, data) {
- const ephemeralPublicKey = _getFirstBytes(data, PUB_KEY_LENGTH);
- const ciphertext = _getBytes(data, PUB_KEY_LENGTH, data.byteLength);
- const agreement = await libsignal.Curve.async.calculateAgreement(
- ephemeralPublicKey,
- staticPrivateKey
- );
-
- const key = await hmacSha256(agreement, uniqueId);
-
- return decryptSymmetric(key, ciphertext);
-}
-
async function deriveAccessKey(profileKey) {
const iv = getZeroes(12);
const plaintext = getZeroes(16);
@@ -206,19 +141,6 @@ async function encryptAesCtr(key, plaintext, counter) {
return ciphertext;
}
-async function decryptAesCtr(key, ciphertext, counter) {
- const extractable = false;
- const algorithm = {
- name: 'AES-CTR',
- counter: new Uint8Array(counter),
- length: 128,
- };
-
- const cryptoKey = await crypto.subtle.importKey('raw', key, algorithm, extractable, ['decrypt']);
- const plaintext = await crypto.subtle.decrypt(algorithm, cryptoKey, ciphertext);
- return plaintext;
-}
-
async function _encrypt_aes_gcm(key, iv, plaintext) {
const algorithm = {
name: 'AES-GCM',
@@ -249,24 +171,6 @@ function getZeroes(n) {
return result;
}
-function highBitsToInt(byte) {
- return (byte & 0xff) >> 4;
-}
-
-function intsToByteHighAndLow(highValue, lowValue) {
- return ((highValue << 4) | lowValue) & 0xff;
-}
-
-function trimBytes(buffer, length) {
- return _getFirstBytes(buffer, length);
-}
-
-function getViewOfArrayBuffer(buffer, start, finish) {
- const source = new Uint8Array(buffer);
- const result = source.slice(start, finish);
- return result.buffer;
-}
-
function concatenateBytes(...elements) {
const length = elements.reduce((total, element) => total + element.byteLength, 0);
@@ -285,32 +189,6 @@ function concatenateBytes(...elements) {
return result.buffer;
}
-function splitBytes(buffer, ...lengths) {
- const total = lengths.reduce((acc, length) => acc + length, 0);
-
- if (total !== buffer.byteLength) {
- throw new Error(
- `Requested lengths total ${total} does not match source total ${buffer.byteLength}`
- );
- }
-
- const source = new Uint8Array(buffer);
- const results = [];
- let position = 0;
-
- for (let i = 0, max = lengths.length; i < max; i += 1) {
- const length = lengths[i];
- const result = new Uint8Array(length);
- const section = source.slice(position, position + length);
- result.set(section);
- position += result.byteLength;
-
- results.push(result);
- }
-
- return results;
-}
-
// Internal-only
function _getFirstBytes(data, n) {
diff --git a/preload.js b/preload.js
index 0bac97019..c8fd8d407 100644
--- a/preload.js
+++ b/preload.js
@@ -345,8 +345,6 @@ window.Signal.Data = require('./ts/data/data');
window.getMessageController = () => window.libsession.Messages.getMessageController();
-// Pulling these in separately since they access filesystem, electron
-window.Signal.Backup = require('./js/modules/backup');
window.Signal.Logs = require('./js/modules/logs');
window.addEventListener('contextmenu', e => {
diff --git a/test/backup_test.js b/test/backup_test.js
deleted file mode 100644
index 526d73ac2..000000000
--- a/test/backup_test.js
+++ /dev/null
@@ -1,524 +0,0 @@
-/* global Signal, assert, _, libsignal */
-
-/* eslint-disable no-console */
-
-'use strict';
-
-describe('Backup', () => {
- describe('_sanitizeFileName', () => {
- it('leaves a basic string alone', () => {
- const initial = "Hello, how are you #5 ('fine' + great).jpg";
- const expected = initial;
- assert.strictEqual(Signal.Backup._sanitizeFileName(initial), expected);
- });
-
- it('replaces all unknown characters', () => {
- const initial = '!@$%^&*=';
- const expected = '________';
- assert.strictEqual(Signal.Backup._sanitizeFileName(initial), expected);
- });
- });
-
- describe('_trimFileName', () => {
- it('handles a file with no extension', () => {
- const initial = '0123456789012345678901234567890123456789';
- const expected = '012345678901234567890123456789';
- assert.strictEqual(Signal.Backup._trimFileName(initial), expected);
- });
-
- it('handles a file with a long extension', () => {
- const initial = '0123456789012345678901234567890123456789.01234567890123456789';
- const expected = '012345678901234567890123456789';
- assert.strictEqual(Signal.Backup._trimFileName(initial), expected);
- });
-
- it('handles a file with a normal extension', () => {
- const initial = '01234567890123456789012345678901234567890123456789.jpg';
- const expected = '012345678901234567890123.jpg';
- assert.strictEqual(Signal.Backup._trimFileName(initial), expected);
- });
- });
-
- describe('_getExportAttachmentFileName', () => {
- it('uses original filename if attachment has one', () => {
- const message = {
- body: 'something',
- };
- const index = 0;
- const attachment = {
- fileName: 'blah.jpg',
- };
- const expected = 'blah.jpg';
-
- const actual = Signal.Backup._getExportAttachmentFileName(message, index, attachment);
- assert.strictEqual(actual, expected);
- });
-
- it('uses attachment id if no filename', () => {
- const message = {
- body: 'something',
- };
- const index = 0;
- const attachment = {
- id: '123',
- };
- const expected = '123';
-
- const actual = Signal.Backup._getExportAttachmentFileName(message, index, attachment);
- assert.strictEqual(actual, expected);
- });
-
- it('uses filename and contentType if available', () => {
- const message = {
- body: 'something',
- };
- const index = 0;
- const attachment = {
- id: '123',
- contentType: 'image/jpeg',
- };
- const expected = '123.jpeg';
-
- const actual = Signal.Backup._getExportAttachmentFileName(message, index, attachment);
- assert.strictEqual(actual, expected);
- });
-
- it('handles strange contentType', () => {
- const message = {
- body: 'something',
- };
- const index = 0;
- const attachment = {
- id: '123',
- contentType: 'something',
- };
- const expected = '123.something';
-
- const actual = Signal.Backup._getExportAttachmentFileName(message, index, attachment);
- assert.strictEqual(actual, expected);
- });
- });
-
- describe('_getAnonymousAttachmentFileName', () => {
- it('uses message id', () => {
- const message = {
- id: 'id-45',
- body: 'something',
- };
- const index = 0;
- const attachment = {
- fileName: 'blah.jpg',
- };
- const expected = 'id-45';
-
- const actual = Signal.Backup._getAnonymousAttachmentFileName(message, index, attachment);
- assert.strictEqual(actual, expected);
- });
-
- it('appends index if it is above zero', () => {
- const message = {
- id: 'id-45',
- body: 'something',
- };
- const index = 1;
- const attachment = {
- fileName: 'blah.jpg',
- };
- const expected = 'id-45-1';
-
- const actual = Signal.Backup._getAnonymousAttachmentFileName(message, index, attachment);
- assert.strictEqual(actual, expected);
- });
- });
-
- describe('_getConversationDirName', () => {
- it('uses name if available', () => {
- const conversation = {
- active_at: 123,
- name: '0123456789012345678901234567890123456789',
- id: 'id',
- };
- const expected = '123 (012345678901234567890123456789 id)';
- assert.strictEqual(Signal.Backup._getConversationDirName(conversation), expected);
- });
-
- it('uses just id if name is not available', () => {
- const conversation = {
- active_at: 123,
- id: 'id',
- };
- const expected = '123 (id)';
- assert.strictEqual(Signal.Backup._getConversationDirName(conversation), expected);
- });
-
- it('uses inactive for missing active_at', () => {
- const conversation = {
- name: 'name',
- id: 'id',
- };
- const expected = 'inactive (name id)';
- assert.strictEqual(Signal.Backup._getConversationDirName(conversation), expected);
- });
- });
-
- describe('_getConversationLoggingName', () => {
- it('uses plain id if conversation is private', () => {
- const conversation = {
- active_at: 123,
- id: 'id',
- type: 'private',
- };
- const expected = '123 (id)';
- assert.strictEqual(Signal.Backup._getConversationLoggingName(conversation), expected);
- });
-
- it('uses just id if name is not available', () => {
- const conversation = {
- active_at: 123,
- id: 'groupId',
- type: 'group',
- };
- const expected = '123 ([REDACTED_GROUP]pId)';
- assert.strictEqual(Signal.Backup._getConversationLoggingName(conversation), expected);
- });
-
- it('uses inactive for missing active_at', () => {
- const conversation = {
- id: 'id',
- type: 'private',
- };
- const expected = 'inactive (id)';
- assert.strictEqual(Signal.Backup._getConversationLoggingName(conversation), expected);
- });
- });
-
- describe('end-to-end', () => {
- it('exports then imports to produce the same data we started with', async function thisNeeded() {
- this.timeout(6000);
-
- const { attachmentsPath, fse, glob, path, tmp, isWindows } = window.test;
-
- // Skip this test on windows
- // because it always fails due to lstat permission error.
- // Don't know how to fix it so this is a temp work around.
- if (isWindows || !isWindows) {
- console.log('Skipping exports then imports to produce the same data we started');
- this.skip();
- return;
- }
-
- const { upgradeMessageSchema, loadAttachmentData } = window.Signal.Migrations;
-
- const staticKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
- const attachmentsPattern = path.join(attachmentsPath, '**');
-
- const OUR_NUMBER = '+12025550000';
- const CONTACT_ONE_NUMBER = '+12025550001';
-
- const toArrayBuffer = nodeBuffer =>
- nodeBuffer.buffer.slice(
- nodeBuffer.byteOffset,
- nodeBuffer.byteOffset + nodeBuffer.byteLength
- );
-
- const getFixture = target => toArrayBuffer(fse.readFileSync(target));
-
- const FIXTURES = {
- gif: getFixture('fixtures/giphy-7GFfijngKbeNy.gif'),
- mp4: getFixture('fixtures/pixabay-Soap-Bubble-7141.mp4'),
- jpg: getFixture('fixtures/koushik-chowdavarapu-105425-unsplash.jpg'),
- mp3: getFixture('fixtures/incompetech-com-Agnus-Dei-X.mp3'),
- txt: getFixture('fixtures/lorem-ipsum.txt'),
- png: getFixture('fixtures/freepngs-2cd43b_bed7d1327e88454487397574d87b64dc_mv2.png'),
- };
-
- async function wrappedLoadAttachment(attachment) {
- return _.omit(await loadAttachmentData(attachment), ['path']);
- }
-
- async function clearAllData() {
- await window.Signal.Data.removeAll();
-
- window.storage.reset();
- await window.storage.fetch();
-
- window.getConversationController().reset();
- window.BlockedNumberController.reset();
- await window.getConversationController().load();
- await window.BlockedNumberController.load();
- await fse.emptyDir(attachmentsPath);
- }
-
- function removeId(model) {
- return _.omit(model, ['id']);
- }
-
- const getUndefinedKeys = object =>
- Object.entries(object)
- .filter(([, value]) => value === undefined)
- .map(([name]) => name);
- const omitUndefinedKeys = object => _.omit(object, getUndefinedKeys(object));
-
- // We want to know which paths have two slashes, since that tells us which files
- // in the attachment fan-out are files vs. directories.
- const TWO_SLASHES = /[^/]*\/[^/]*\/[^/]*/;
- // On windows, attachmentsPath has a normal windows path format (\ separators), but
- // glob returns only /. We normalize to / separators for our manipulations.
- const normalizedBase = attachmentsPath.replace(/\\/g, '/');
- function removeDirs(dirs) {
- return _.filter(dirs, fullDir => {
- const dir = fullDir.replace(normalizedBase, '');
- return TWO_SLASHES.test(dir);
- });
- }
-
- function _mapQuotedAttachments(mapper) {
- return async (message, context) => {
- if (!message.quote) {
- return message;
- }
-
- const wrappedMapper = async attachment => {
- if (!attachment || !attachment.thumbnail) {
- return attachment;
- }
-
- return Object.assign({}, attachment, {
- thumbnail: await mapper(attachment.thumbnail, context),
- });
- };
-
- const quotedAttachments = (message.quote && message.quote.attachments) || [];
-
- return Object.assign({}, message, {
- quote: Object.assign({}, message.quote, {
- attachments: await Promise.all(quotedAttachments.map(wrappedMapper)),
- }),
- });
- };
- }
-
- async function loadAllFilesFromDisk(message) {
- const loadThumbnails = _mapQuotedAttachments(thumbnail => {
- // we want to be bulletproof to thumbnails without data
- if (!thumbnail.path) {
- return thumbnail;
- }
-
- return wrappedLoadAttachment(thumbnail);
- });
-
- return Object.assign({}, await loadThumbnails(message), {
- attachments: await Promise.all(
- (message.attachments || []).map(async attachment => {
- await wrappedLoadAttachment(attachment);
-
- if (attachment.thumbnail) {
- await wrappedLoadAttachment(attachment.thumbnail);
- }
-
- if (attachment.screenshot) {
- await wrappedLoadAttachment(attachment.screenshot);
- }
-
- return attachment;
- })
- ),
- preview: await Promise.all(
- (message.preview || []).map(async item => {
- if (item.image) {
- await wrappedLoadAttachment(item.image);
- }
-
- return item;
- })
- ),
- });
- }
-
- let backupDir;
- try {
- // Seven total:
- // - Five from image/video attachments
- // - One from embedded contact avatar
- // - One from embedded quoted attachment thumbnail
- // - One from a link preview image
- const ATTACHMENT_COUNT = 8;
- const MESSAGE_COUNT = 1;
- const CONVERSATION_COUNT = 1;
-
- const messageWithAttachments = {
- conversationId: CONTACT_ONE_NUMBER,
- body: 'Totally!',
- source: OUR_NUMBER,
- received_at: 1524185933350,
- timestamp: 1524185933350,
- errors: [],
- attachments: [
- // Note: generates two more files: screenshot and thumbnail
- {
- contentType: 'video/mp4',
- fileName: 'video.mp4',
- data: FIXTURES.mp4,
- },
- // Note: generates one more file: thumbnail
- {
- contentType: 'image/png',
- fileName: 'landscape.png',
- data: FIXTURES.png,
- },
- ],
- hasAttachments: 1,
- hasVisualMediaAttachments: 1,
- quote: {
- text: "Isn't it cute?",
- author: CONTACT_ONE_NUMBER,
- id: 12345678,
- attachments: [
- {
- contentType: 'audio/mp3',
- fileName: 'song.mp3',
- },
- {
- contentType: 'image/gif',
- fileName: 'avatar.gif',
- thumbnail: {
- contentType: 'image/png',
- data: FIXTURES.gif,
- },
- },
- ],
- },
-
- preview: [
- {
- url: 'https://www.instagram.com/p/BsOGulcndj-/',
- title:
- 'EGG GANG π on Instagram: βLetβs set a world record together and get the most liked post on Instagram. Beating the current world record held by Kylie Jenner (18β¦β',
- image: {
- contentType: 'image/jpeg',
- data: FIXTURES.jpg,
- },
- },
- ],
- };
-
- console.log('Backup test: Clear all data');
- await clearAllData();
-
- console.log('Backup test: Create models, save to db/disk');
- const message = await upgradeMessageSchema(messageWithAttachments);
- await window.Signal.Data.saveMessage(message);
-
- const conversation = {
- active_at: 1524185933350,
- color: 'orange',
- expireTimer: 0,
- id: CONTACT_ONE_NUMBER,
- name: 'Someone Somewhere',
- profileAvatar: {
- contentType: 'image/jpeg',
- data: FIXTURES.jpeg,
- size: 64,
- },
- profileKey: 'BASE64KEY',
- profileName: 'Someone! π€',
- timestamp: 1524185933350,
- type: 'private',
- unreadCount: 0,
- version: 2,
- };
- console.log({ conversation });
- await window.Signal.Data.saveConversation(conversation);
-
- console.log('Backup test: Ensure that all attachments were saved to disk');
- const attachmentFiles = removeDirs(glob.sync(attachmentsPattern));
- console.log({ attachmentFiles });
- assert.strictEqual(ATTACHMENT_COUNT, attachmentFiles.length);
-
- console.log('Backup test: Export!');
- backupDir = tmp.dirSync().name;
- console.log({ backupDir });
- await Signal.Backup.exportToDirectory(backupDir, {
- key: staticKeyPair.pubKey,
- });
-
- console.log('Backup test: Ensure that messages.tar.gz exists');
- const archivePath = path.join(backupDir, 'messages.tar.gz');
- const messageZipExists = fse.existsSync(archivePath);
- assert.strictEqual(true, messageZipExists);
-
- console.log('Backup test: Ensure that all attachments made it to backup dir');
- const backupAttachmentPattern = path.join(backupDir, 'attachments/*');
- const backupAttachments = glob.sync(backupAttachmentPattern);
- console.log({ backupAttachments });
- assert.strictEqual(ATTACHMENT_COUNT, backupAttachments.length);
-
- console.log('Backup test: Clear all data');
- await clearAllData();
-
- console.log('Backup test: Import!');
- await Signal.Backup.importFromDirectory(backupDir, {
- key: staticKeyPair.privKey,
- });
-
- console.log('Backup test: Check conversations');
- const conversationCollection = await window.Signal.Data.getAllConversations();
- assert.strictEqual(conversationCollection.length, CONVERSATION_COUNT);
-
- // We need to ommit any custom fields we have added
- const ommited = [
- 'profileAvatar',
- 'swarmNodes',
- 'groupAdmins',
- 'isKickedFromGroup',
- 'unlockTimestamp',
- 'sessionResetStatus',
- 'isOnline',
- ];
- const conversationFromDB = conversationCollection.at(0).attributes;
- console.log({ conversationFromDB, conversation });
- assert.deepEqual(_.omit(conversationFromDB, ommited), _.omit(conversation, ommited));
-
- console.log('Backup test: Check messages');
- const allMessages = await window.Signal.Data.getAllMessages();
- assert.strictEqual(allMessages.length, MESSAGE_COUNT);
- const messageFromDB = removeId(allMessages.at(0).attributes);
- const expectedMessage = messageFromDB;
- console.log({ messageFromDB, expectedMessage });
- assert.deepEqual(messageFromDB, expectedMessage);
-
- console.log('Backup test: ensure that all attachments were imported');
- const recreatedAttachmentFiles = removeDirs(glob.sync(attachmentsPattern));
- console.log({ recreatedAttachmentFiles });
- assert.strictEqual(ATTACHMENT_COUNT, recreatedAttachmentFiles.length);
- assert.deepEqual(attachmentFiles, recreatedAttachmentFiles);
-
- console.log('Backup test: Check that all attachments were successfully imported');
- const messageWithAttachmentsFromDB = await loadAllFilesFromDisk(messageFromDB);
- const expectedMessageWithAttachments = await loadAllFilesFromDisk(
- omitUndefinedKeys(message)
- );
- console.log({
- messageWithAttachmentsFromDB,
- expectedMessageWithAttachments,
- });
- assert.deepEqual(
- _.omit(messageWithAttachmentsFromDB, ['sent']),
- expectedMessageWithAttachments
- );
-
- console.log('Backup test: Clear all data');
- await clearAllData();
-
- console.log('Backup test: Complete!');
- } finally {
- if (backupDir) {
- console.log({ backupDir });
- console.log('Deleting', backupDir);
- await fse.remove(backupDir);
- }
- }
- });
- });
-});
diff --git a/test/index.html b/test/index.html
index dc14ba068..cb961f017 100644
--- a/test/index.html
+++ b/test/index.html
@@ -52,7 +52,6 @@
-