Merge branch 'development'

pull/19/head
Scott Nonnenberg 7 years ago
commit bff4b27fa7

@ -12,6 +12,7 @@ test/views/*.js
# Generated files # Generated files
js/components.js js/components.js
js/libtextsecure.js js/libtextsecure.js
js/util_worker.js
js/libsignal-protocol-worker.js js/libsignal-protocol-worker.js
libtextsecure/components.js libtextsecure/components.js
libtextsecure/test/test.js libtextsecure/test/test.js

1
.gitignore vendored

@ -17,6 +17,7 @@ sql/
# generated files # generated files
js/components.js js/components.js
js/util_worker.js
js/libtextsecure.js js/libtextsecure.js
libtextsecure/components.js libtextsecure/components.js
libtextsecure/test/test.js libtextsecure/test/test.js

@ -6,6 +6,7 @@ config/local-*.json
config/local.json config/local.json
dist/** dist/**
js/components.js js/components.js
js/util_worker.js
js/libtextsecure.js js/libtextsecure.js
libtextsecure/components.js libtextsecure/components.js
libtextsecure/test/test.js libtextsecure/test/test.js

@ -33,6 +33,14 @@ module.exports = grunt => {
src: components, src: components,
dest: 'js/components.js', dest: 'js/components.js',
}, },
util_worker: {
src: [
'components/bytebuffer/dist/ByteBufferAB.js',
'components/long/dist/Long.js',
'js/util_worker_tasks.js',
],
dest: 'js/util_worker.js',
},
libtextsecurecomponents: { libtextsecurecomponents: {
src: libtextsecurecomponents, src: libtextsecurecomponents,
dest: 'libtextsecure/components.js', dest: 'libtextsecure/components.js',

File diff suppressed because it is too large Load Diff

@ -11,8 +11,9 @@ module.exports = {
let initialized = false; let initialized = false;
const ERASE_ATTACHMENTS_KEY = 'erase-attachments'; const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
async function initialize({ configDir }) { async function initialize({ configDir, cleanupOrphanedAttachments }) {
if (initialized) { if (initialized) {
throw new Error('initialze: Already initialized!'); throw new Error('initialze: Already initialized!');
} }
@ -29,8 +30,19 @@ async function initialize({ configDir }) {
event.sender.send(`${ERASE_ATTACHMENTS_KEY}-done`); event.sender.send(`${ERASE_ATTACHMENTS_KEY}-done`);
} catch (error) { } catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error; const errorForDisplay = error && error.stack ? error.stack : error;
console.log(`sql-erase error: ${errorForDisplay}`); console.log(`erase attachments error: ${errorForDisplay}`);
event.sender.send(`${ERASE_ATTACHMENTS_KEY}-done`, error); event.sender.send(`${ERASE_ATTACHMENTS_KEY}-done`, error);
} }
}); });
ipcMain.on(CLEANUP_ORPHANED_ATTACHMENTS_KEY, async event => {
try {
await cleanupOrphanedAttachments();
event.sender.send(`${CLEANUP_ORPHANED_ATTACHMENTS_KEY}-done`);
} catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error;
console.log(`cleanup orphaned attachments error: ${errorForDisplay}`);
event.sender.send(`${CLEANUP_ORPHANED_ATTACHMENTS_KEY}-done`, error);
}
});
} }

@ -0,0 +1,58 @@
const fs = require('fs');
const _ = require('lodash');
const ENCODING = 'utf8';
module.exports = {
start,
};
function start(name, targetPath) {
let cachedValue = null;
try {
const text = fs.readFileSync(targetPath, ENCODING);
cachedValue = JSON.parse(text);
console.log(`config/get: Successfully read ${name} config file`);
if (!cachedValue) {
console.log(
`config/get: ${name} config value was falsy, cache is now empty object`
);
cachedValue = Object.create(null);
}
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
console.log(
`config/get: Did not find ${name} config file, cache is now empty object`
);
cachedValue = Object.create(null);
}
function get(keyPath) {
return _.get(cachedValue, keyPath);
}
function set(keyPath, value) {
_.set(cachedValue, keyPath, value);
console.log(`config/set: Saving ${name} config to disk`);
const text = JSON.stringify(cachedValue, null, ' ');
fs.writeFileSync(targetPath, text, ENCODING);
}
function remove() {
console.log(`config/remove: Deleting ${name} config from disk`);
fs.unlinkSync(targetPath);
cachedValue = Object.create(null);
}
return {
set,
get,
remove,
};
}

@ -0,0 +1,12 @@
const path = require('path');
const { app } = require('electron');
const { start } = require('./base_config');
const userDataPath = app.getPath('userData');
const targetPath = path.join(userDataPath, 'ephemeral.json');
const ephemeralConfig = start('ephemeral', targetPath);
module.exports = ephemeralConfig;

@ -1,5 +1,7 @@
const electron = require('electron'); const electron = require('electron');
const sql = require('./sql'); const sql = require('./sql');
const { remove: removeUserConfig } = require('./user_config');
const { remove: removeEphemeralConfig } = require('./ephemeral_config');
const { ipcMain } = electron; const { ipcMain } = electron;
@ -12,16 +14,12 @@ let initialized = false;
const SQL_CHANNEL_KEY = 'sql-channel'; const SQL_CHANNEL_KEY = 'sql-channel';
const ERASE_SQL_KEY = 'erase-sql-key'; const ERASE_SQL_KEY = 'erase-sql-key';
function initialize({ userConfig }) { function initialize() {
if (initialized) { if (initialized) {
throw new Error('sqlChannels: already initialized!'); throw new Error('sqlChannels: already initialized!');
} }
initialized = true; initialized = true;
if (!userConfig) {
throw new Error('initialize: userConfig is required!');
}
ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => { ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => {
try { try {
const fn = sql[callName]; const fn = sql[callName];
@ -44,7 +42,8 @@ function initialize({ userConfig }) {
ipcMain.on(ERASE_SQL_KEY, async event => { ipcMain.on(ERASE_SQL_KEY, async event => {
try { try {
userConfig.set('key', null); removeUserConfig();
removeEphemeralConfig();
event.sender.send(`${ERASE_SQL_KEY}-done`); event.sender.send(`${ERASE_SQL_KEY}-done`);
} catch (error) { } catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error; const errorForDisplay = error && error.stack ? error.stack : error;

@ -1,11 +1,11 @@
const path = require('path'); const path = require('path');
const { app } = require('electron'); const { app } = require('electron');
const ElectronConfig = require('electron-config');
const { start } = require('./base_config');
const config = require('./config'); const config = require('./config');
// use a separate data directory for development // Use separate data directory for development
if (config.has('storageProfile')) { if (config.has('storageProfile')) {
const userData = path.join( const userData = path.join(
app.getPath('appData'), app.getPath('appData'),
@ -17,7 +17,9 @@ if (config.has('storageProfile')) {
console.log(`userData: ${app.getPath('userData')}`); console.log(`userData: ${app.getPath('userData')}`);
// this needs to be below our update to the appData path const userDataPath = app.getPath('userData');
const userConfig = new ElectronConfig(); const targetPath = path.join(userDataPath, 'config.json');
const userConfig = start('user', targetPath);
module.exports = userConfig; module.exports = userConfig;

@ -408,6 +408,10 @@
); );
window.log.info('Cleanup: complete'); window.log.info('Cleanup: complete');
if (newVersion) {
await window.Signal.Data.cleanupOrphanedAttachments();
}
Views.Initialization.setMessage(window.i18n('loading')); Views.Initialization.setMessage(window.i18n('loading'));
// Note: We are not invoking the second set of IndexedDB migrations because it is // Note: We are not invoking the second set of IndexedDB migrations because it is
@ -1105,9 +1109,10 @@
conversationId: data.destination, conversationId: data.destination,
type: 'outgoing', type: 'outgoing',
sent: true, sent: true,
expirationStartTimestamp: data.expirationStartTimestamp expirationStartTimestamp: Math.min(
? Math.min(data.expirationStartTimestamp, Date.now()) data.expirationStartTimestamp || data.timestamp || Date.now(),
: null, Date.now()
),
}); });
} }

@ -166,15 +166,18 @@
this.updateLastMessage(); this.updateLastMessage();
const removeMessage = () => { const removeMessage = () => {
const existing = this.messageCollection.get(message.id); const { id } = message;
const existing = this.messageCollection.get(id);
if (!existing) { if (!existing) {
return; return;
} }
window.log.info('Remove expired message from collection', { window.log.info('Remove expired message from collection', {
sentAt: message.get('sent_at'), sentAt: existing.get('sent_at'),
}); });
this.messageCollection.remove(message.id);
this.messageCollection.remove(id);
existing.trigger('expired');
}; };
// If a fetch is in progress, then we need to wait until that's complete to // If a fetch is in progress, then we need to wait until that's complete to

@ -83,6 +83,7 @@
this.on('change:expirationStartTimestamp', this.setToExpire); this.on('change:expirationStartTimestamp', this.setToExpire);
this.on('change:expireTimer', this.setToExpire); this.on('change:expireTimer', this.setToExpire);
this.on('unload', this.unload); this.on('unload', this.unload);
this.on('expired', this.onExpired);
this.setToExpire(); this.setToExpire();
}, },
idForLogging() { idForLogging() {
@ -233,7 +234,9 @@
this.quotedMessage = null; this.quotedMessage = null;
} }
}, },
onExpired() {
this.hasExpired = true;
},
getPropsForTimerNotification() { getPropsForTimerNotification() {
const { expireTimer, fromSync, source } = this.get( const { expireTimer, fromSync, source } = this.get(
'expirationTimerUpdate' 'expirationTimerUpdate'
@ -424,6 +427,7 @@
attachment: this.getPropsForAttachment(firstAttachment), attachment: this.getPropsForAttachment(firstAttachment),
quote: this.getPropsForQuote(), quote: this.getPropsForQuote(),
authorAvatarPath, authorAvatarPath,
isExpired: this.hasExpired,
expirationLength, expirationLength,
expirationTimestamp, expirationTimestamp,
onReply: () => this.trigger('reply', this), onReply: () => this.trigger('reply', this),
@ -875,17 +879,33 @@
promises.push(c.getProfiles()); promises.push(c.getProfiles());
} }
} else { } else {
this.saveErrors(result.errors);
if (result.successfulNumbers.length > 0) { if (result.successfulNumbers.length > 0) {
const sentTo = this.get('sent_to') || []; const sentTo = this.get('sent_to') || [];
// Note: In a partially-successful group send, we do not start // In groups, we don't treat unregistered users as a user-visible
// the expiration timer. // error. The message will look successful, but the details
// screen will show that we didn't send to these unregistered users.
const filteredErrors = _.reject(
result.errors,
error => error.name === 'UnregisteredUserError'
);
// We don't start the expiration timer if there are real errors
// left after filtering out all of the unregistered user errors.
const expirationStartTimestamp = filteredErrors.length
? null
: Date.now();
this.saveErrors(filteredErrors);
this.set({ this.set({
sent_to: _.union(sentTo, result.successfulNumbers), sent_to: _.union(sentTo, result.successfulNumbers),
sent: true, sent: true,
expirationStartTimestamp,
}); });
promises.push(this.sendSyncMessage()); promises.push(this.sendSyncMessage());
} else {
this.saveErrors(result.errors);
} }
promises = promises.concat( promises = promises.concat(
_.map(result.errors, error => { _.map(result.errors, error => {

@ -22,6 +22,7 @@ const DATABASE_UPDATE_TIMEOUT = 2 * 60 * 1000; // two minutes
const SQL_CHANNEL_KEY = 'sql-channel'; const SQL_CHANNEL_KEY = 'sql-channel';
const ERASE_SQL_KEY = 'erase-sql-key'; const ERASE_SQL_KEY = 'erase-sql-key';
const ERASE_ATTACHMENTS_KEY = 'erase-attachments'; const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
const _jobs = Object.create(null); const _jobs = Object.create(null);
const _DEBUG = false; const _DEBUG = false;
@ -64,6 +65,7 @@ module.exports = {
removeAll, removeAll,
removeOtherData, removeOtherData,
cleanupOrphanedAttachments,
// Returning plain JSON // Returning plain JSON
getMessagesNeedingUpgrade, getMessagesNeedingUpgrade,
@ -164,7 +166,7 @@ ipcRenderer.on(
const job = _getJob(jobId); const job = _getJob(jobId);
if (!job) { if (!job) {
throw new Error( throw new Error(
`Received job reply to job ${jobId}, but did not have it in our registry!` `Received SQL channel reply to job ${jobId}, but did not have it in our registry!`
); );
} }
@ -172,7 +174,9 @@ ipcRenderer.on(
if (errorForDisplay) { if (errorForDisplay) {
return reject( return reject(
new Error(`Error calling channel ${fnName}: ${errorForDisplay}`) new Error(
`Error received from SQL channel job ${jobId} (${fnName}): ${errorForDisplay}`
)
); );
} }
@ -194,7 +198,8 @@ function makeChannel(fnName) {
}); });
setTimeout( setTimeout(
() => reject(new Error(`Request to ${fnName} timed out`)), () =>
reject(new Error(`SQL channel job ${jobId} (${fnName}) timed out`)),
DATABASE_UPDATE_TIMEOUT DATABASE_UPDATE_TIMEOUT
); );
}); });
@ -244,8 +249,7 @@ async function removeMessage(id, { Message }) {
// it needs to delete all associated on-disk files along with the database delete. // it needs to delete all associated on-disk files along with the database delete.
if (message) { if (message) {
await channels.removeMessage(id); await channels.removeMessage(id);
const model = new Message(message); await message.cleanup();
await model.cleanup();
} }
} }
@ -388,6 +392,10 @@ async function removeAll() {
await channels.removeAll(); await channels.removeAll();
} }
async function cleanupOrphanedAttachments() {
await callChannel(CLEANUP_ORPHANED_ATTACHMENTS_KEY);
}
// Note: will need to restart the app after calling this, to set up afresh // Note: will need to restart the app after calling this, to set up afresh
async function removeOtherData() { async function removeOtherData() {
await Promise.all([ await Promise.all([

@ -1,6 +1,6 @@
/* global window, IDBKeyRange */ /* global window, IDBKeyRange */
const { includes, isFunction, isString, last, forEach } = require('lodash'); const { includes, isFunction, isString, last, map } = require('lodash');
const { const {
saveMessages, saveMessages,
_removeMessages, _removeMessages,
@ -83,23 +83,25 @@ async function migrateToSQL({
const status = await migrateStoreToSQLite({ const status = await migrateStoreToSQLite({
db, db,
save: async array => { save: async array => {
forEach(array, item => { await Promise.all(
// In the new database, we can't store ArrayBuffers, so we turn these two fields map(array, async item => {
// into strings like MessageReceiver now does before save. // In the new database, we can't store ArrayBuffers, so we turn these two
// fields into strings like MessageReceiver now does before save.
// Need to set it to version two, since we're using Base64 strings now // Need to set it to version two, since we're using Base64 strings now
// eslint-disable-next-line no-param-reassign
item.version = 2;
if (item.envelope) {
// eslint-disable-next-line no-param-reassign
item.envelope = arrayBufferToString(item.envelope);
}
if (item.decrypted) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
item.decrypted = arrayBufferToString(item.decrypted); item.version = 2;
}
}); if (item.envelope) {
// eslint-disable-next-line no-param-reassign
item.envelope = await arrayBufferToString(item.envelope);
}
if (item.decrypted) {
// eslint-disable-next-line no-param-reassign
item.decrypted = await arrayBufferToString(item.decrypted);
}
})
);
await saveUnprocesseds(array); await saveUnprocesseds(array);
}, },
remove: removeUnprocessed, remove: removeUnprocessed,

@ -0,0 +1,44 @@
/* global dcodeIO */
/* eslint-disable strict */
'use strict';
const functions = {
stringToArrayBufferBase64,
arrayBufferToStringBase64,
};
onmessage = async e => {
const [jobId, fnName, ...args] = e.data;
try {
const fn = functions[fnName];
if (!fn) {
throw new Error(`Worker: job ${jobId} did not find function ${fnName}`);
}
const result = await fn(...args);
postMessage([jobId, null, result]);
} catch (error) {
const errorForDisplay = prepareErrorForPostMessage(error);
postMessage([jobId, errorForDisplay]);
}
};
function prepareErrorForPostMessage(error) {
if (!error) {
return null;
}
if (error.stack) {
return error.stack;
}
return error.message;
}
function stringToArrayBufferBase64(string) {
return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();
}
function arrayBufferToStringBase64(arrayBuffer) {
return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
}

@ -34,11 +34,15 @@
this.render(); this.render();
try { try {
await Database.clear();
await Database.close(); await Database.close();
window.log.info('All database connections closed. Starting delete.'); window.log.info(
'All database connections closed. Starting database drop.'
);
await Database.drop();
} catch (error) { } catch (error) {
window.log.error( window.log.error(
'Something went wrong closing all database connections.' 'Something went wrong deleting IndexedDB data then dropping database.'
); );
} }
@ -46,15 +50,14 @@
}, },
async clearAllData() { async clearAllData() {
try { try {
await Promise.all([ await Logs.deleteAll();
Logs.deleteAll(),
Database.drop(),
window.Signal.Data.removeAll(),
window.Signal.Data.removeOtherData(),
]);
// SQLCipher
await window.Signal.Data.removeAll();
await window.Signal.Data.close(); await window.Signal.Data.close();
await window.Signal.Data.removeDB(); await window.Signal.Data.removeDB();
await window.Signal.Data.removeOtherData();
} catch (error) { } catch (error) {
window.log.error( window.log.error(
'Something went wrong deleting all data:', 'Something went wrong deleting all data:',

@ -15,6 +15,7 @@
this.listenTo(this.model, 'change', this.onChange); this.listenTo(this.model, 'change', this.onChange);
this.listenTo(this.model, 'destroy', this.onDestroy); this.listenTo(this.model, 'destroy', this.onDestroy);
this.listenTo(this.model, 'unload', this.onUnload); this.listenTo(this.model, 'unload', this.onUnload);
this.listenTo(this.model, 'expired', this.onExpired);
}, },
onChange() { onChange() {
this.addId(); this.addId();
@ -27,6 +28,9 @@
const { id } = this.model; const { id } = this.model;
this.$el.attr('id', id); this.$el.attr('id', id);
}, },
onExpired() {
setTimeout(() => this.onUnload(), 1000);
},
onUnload() { onUnload() {
if (this.childView) { if (this.childView) {
this.childView.remove(); this.childView.remove();
@ -93,6 +97,7 @@
}; };
this.listenTo(this.model, 'change', update); this.listenTo(this.model, 'change', update);
this.listenTo(this.model, 'expired', update);
this.conversation = this.model.getConversation(); this.conversation = this.model.getConversation();
this.listenTo(this.conversation, 'change', update); this.listenTo(this.conversation, 'change', update);

@ -9,9 +9,112 @@
/* global _: false */ /* global _: false */
/* global ContactBuffer: false */ /* global ContactBuffer: false */
/* global GroupBuffer: false */ /* global GroupBuffer: false */
/* global Worker: false */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
const WORKER_TIMEOUT = 60 * 1000; // one minute
const _utilWorker = new Worker('js/util_worker.js');
const _jobs = Object.create(null);
const _DEBUG = false;
let _jobCounter = 0;
function _makeJob(fnName) {
_jobCounter += 1;
const id = _jobCounter;
if (_DEBUG) {
window.log.info(`Worker job ${id} (${fnName}) started`);
}
_jobs[id] = {
fnName,
start: Date.now(),
};
return id;
}
function _updateJob(id, data) {
const { resolve, reject } = data;
const { fnName, start } = _jobs[id];
_jobs[id] = {
..._jobs[id],
...data,
resolve: value => {
_removeJob(id);
const end = Date.now();
window.log.info(
`Worker job ${id} (${fnName}) succeeded in ${end - start}ms`
);
return resolve(value);
},
reject: error => {
_removeJob(id);
const end = Date.now();
window.log.info(
`Worker job ${id} (${fnName}) failed in ${end - start}ms`
);
return reject(error);
},
};
}
function _removeJob(id) {
if (_DEBUG) {
_jobs[id].complete = true;
} else {
delete _jobs[id];
}
}
function _getJob(id) {
return _jobs[id];
}
async function callWorker(fnName, ...args) {
const jobId = _makeJob(fnName);
return new Promise((resolve, reject) => {
_utilWorker.postMessage([jobId, fnName, ...args]);
_updateJob(jobId, {
resolve,
reject,
args: _DEBUG ? args : null,
});
setTimeout(
() => reject(new Error(`Worker job ${jobId} (${fnName}) timed out`)),
WORKER_TIMEOUT
);
});
}
_utilWorker.onmessage = e => {
const [jobId, errorForDisplay, result] = e.data;
const job = _getJob(jobId);
if (!job) {
throw new Error(
`Received worker reply to job ${jobId}, but did not have it in our registry!`
);
}
const { resolve, reject, fnName } = job;
if (errorForDisplay) {
return reject(
new Error(
`Error received from worker job ${jobId} (${fnName}): ${errorForDisplay}`
)
);
}
return resolve(result);
};
function MessageReceiver(username, password, signalingKey, options = {}) { function MessageReceiver(username, password, signalingKey, options = {}) {
this.count = 0; this.count = 0;
@ -32,13 +135,14 @@ function MessageReceiver(username, password, signalingKey, options = {}) {
} }
MessageReceiver.stringToArrayBuffer = string => MessageReceiver.stringToArrayBuffer = string =>
dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer(); Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer());
MessageReceiver.arrayBufferToString = arrayBuffer => MessageReceiver.arrayBufferToString = arrayBuffer =>
dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary'); Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary'));
MessageReceiver.stringToArrayBufferBase64 = string => MessageReceiver.stringToArrayBufferBase64 = string =>
dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer(); callWorker('stringToArrayBufferBase64', string);
MessageReceiver.arrayBufferToStringBase64 = arrayBuffer => MessageReceiver.arrayBufferToStringBase64 = arrayBuffer =>
dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64'); callWorker('arrayBufferToStringBase64', arrayBuffer);
MessageReceiver.prototype = new textsecure.EventTarget(); MessageReceiver.prototype = new textsecure.EventTarget();
MessageReceiver.prototype.extend({ MessageReceiver.prototype.extend({
@ -271,13 +375,8 @@ MessageReceiver.prototype.extend({
async queueAllCached() { async queueAllCached() {
const items = await this.getAllFromCache(); const items = await this.getAllFromCache();
for (let i = 0, max = items.length; i < max; i += 1) { for (let i = 0, max = items.length; i < max; i += 1) {
if (i > 0 && i % 20 === 0) { // eslint-disable-next-line no-await-in-loop
window.log.info('queueAllCached: Giving event loop a rest'); await this.queueCached(items[i]);
// eslint-disable-next-line no-await-in-loop
await new Promise(resolve => setTimeout(resolve, 2000));
}
this.queueCached(items[i]);
} }
}, },
async queueCached(item) { async queueCached(item) {
@ -285,13 +384,13 @@ MessageReceiver.prototype.extend({
let envelopePlaintext = item.envelope; let envelopePlaintext = item.envelope;
if (item.version === 2) { if (item.version === 2) {
envelopePlaintext = MessageReceiver.stringToArrayBufferBase64( envelopePlaintext = await MessageReceiver.stringToArrayBufferBase64(
envelopePlaintext envelopePlaintext
); );
} }
if (typeof envelopePlaintext === 'string') { if (typeof envelopePlaintext === 'string') {
envelopePlaintext = MessageReceiver.stringToArrayBuffer( envelopePlaintext = await MessageReceiver.stringToArrayBuffer(
envelopePlaintext envelopePlaintext
); );
} }
@ -302,13 +401,13 @@ MessageReceiver.prototype.extend({
let payloadPlaintext = decrypted; let payloadPlaintext = decrypted;
if (item.version === 2) { if (item.version === 2) {
payloadPlaintext = MessageReceiver.stringToArrayBufferBase64( payloadPlaintext = await MessageReceiver.stringToArrayBufferBase64(
payloadPlaintext payloadPlaintext
); );
} }
if (typeof payloadPlaintext === 'string') { if (typeof payloadPlaintext === 'string') {
payloadPlaintext = MessageReceiver.stringToArrayBuffer( payloadPlaintext = await MessageReceiver.stringToArrayBuffer(
payloadPlaintext payloadPlaintext
); );
} }
@ -375,12 +474,12 @@ MessageReceiver.prototype.extend({
); );
}); });
}, },
addToCache(envelope, plaintext) { async addToCache(envelope, plaintext) {
const id = this.getEnvelopeId(envelope); const id = this.getEnvelopeId(envelope);
const data = { const data = {
id, id,
version: 2, version: 2,
envelope: MessageReceiver.arrayBufferToStringBase64(plaintext), envelope: await MessageReceiver.arrayBufferToStringBase64(plaintext),
timestamp: Date.now(), timestamp: Date.now(),
attempts: 1, attempts: 1,
}; };
@ -399,10 +498,13 @@ MessageReceiver.prototype.extend({
if (item.get('version') === 2) { if (item.get('version') === 2) {
item.set( item.set(
'decrypted', 'decrypted',
MessageReceiver.arrayBufferToStringBase64(plaintext) await MessageReceiver.arrayBufferToStringBase64(plaintext)
); );
} else { } else {
item.set('decrypted', MessageReceiver.arrayBufferToString(plaintext)); item.set(
'decrypted',
await MessageReceiver.arrayBufferToString(plaintext)
);
} }
return textsecure.storage.unprocessed.save(item.attributes); return textsecure.storage.unprocessed.save(item.attributes);

@ -10,8 +10,12 @@ const _ = require('lodash');
const pify = require('pify'); const pify = require('pify');
const electron = require('electron'); const electron = require('electron');
const getRealPath = pify(fs.realpath); const packageJson = require('./package.json');
const GlobalErrors = require('./app/global_errors');
GlobalErrors.addHandler();
const getRealPath = pify(fs.realpath);
const { const {
app, app,
BrowserWindow, BrowserWindow,
@ -22,26 +26,6 @@ const {
shell, shell,
} = electron; } = electron;
const packageJson = require('./package.json');
const sql = require('./app/sql');
const sqlChannels = require('./app/sql_channel');
// const attachments = require('./app/attachments');
const attachmentChannel = require('./app/attachment_channel');
const autoUpdate = require('./app/auto_update');
const createTrayIcon = require('./app/tray_icon');
const GlobalErrors = require('./app/global_errors');
const logging = require('./app/logging');
const windowState = require('./app/window_state');
const { createTemplate } = require('./app/menu');
const {
installFileHandler,
installWebHandler,
} = require('./app/protocol_filter');
const { installPermissionsHandler } = require('./app/permissions');
GlobalErrors.addHandler();
const appUserModelId = `org.whispersystems.${packageJson.name}`; const appUserModelId = `org.whispersystems.${packageJson.name}`;
console.log('Set Windows Application User Model ID (AUMID)', { console.log('Set Windows Application User Model ID (AUMID)', {
appUserModelId, appUserModelId,
@ -64,14 +48,32 @@ const usingTrayIcon =
const config = require('./app/config'); const config = require('./app/config');
// Very important to put before the single instance check, since it is based on the
// userData directory.
const userConfig = require('./app/user_config');
const importMode = const importMode =
process.argv.some(arg => arg === '--import') || config.get('import'); process.argv.some(arg => arg === '--import') || config.get('import');
const development = config.environment === 'development'; const development = config.environment === 'development';
// Very important to put before the single instance check, since it is based on the // We generally want to pull in our own modules after this point, after the user
// userData directory. // data directory has been set.
const userConfig = require('./app/user_config'); const attachments = require('./app/attachments');
const attachmentChannel = require('./app/attachment_channel');
const autoUpdate = require('./app/auto_update');
const createTrayIcon = require('./app/tray_icon');
const ephemeralConfig = require('./app/ephemeral_config');
const logging = require('./app/logging');
const sql = require('./app/sql');
const sqlChannels = require('./app/sql_channel');
const windowState = require('./app/window_state');
const { createTemplate } = require('./app/menu');
const {
installFileHandler,
installWebHandler,
} = require('./app/protocol_filter');
const { installPermissionsHandler } = require('./app/permissions');
function showWindow() { function showWindow() {
if (!mainWindow) { if (!mainWindow) {
@ -114,7 +116,14 @@ if (!process.mas) {
} }
} }
let windowConfig = userConfig.get('window'); const windowFromUserConfig = userConfig.get('window');
const windowFromEphemeral = ephemeralConfig.get('window');
let windowConfig = windowFromEphemeral || windowFromUserConfig;
if (windowFromUserConfig) {
userConfig.set('window', null);
ephemeralConfig.set('window', windowConfig);
}
const loadLocale = require('./app/locale').load; const loadLocale = require('./app/locale').load;
// Both of these will be set after app fires the 'ready' event // Both of these will be set after app fires the 'ready' event
@ -284,7 +293,7 @@ function createWindow() {
'Updating BrowserWindow config: %s', 'Updating BrowserWindow config: %s',
JSON.stringify(windowConfig) JSON.stringify(windowConfig)
); );
userConfig.set('window', windowConfig); ephemeralConfig.set('window', windowConfig);
} }
const debouncedCaptureStats = _.debounce(captureAndSaveWindowStats, 500); const debouncedCaptureStats = _.debounce(captureAndSaveWindowStats, 500);
@ -618,24 +627,33 @@ app.on('ready', async () => {
locale = loadLocale({ appLocale, logger }); locale = loadLocale({ appLocale, logger });
} }
await attachmentChannel.initialize({ configDir: userDataPath });
let key = userConfig.get('key'); let key = userConfig.get('key');
if (!key) { if (!key) {
console.log(
'key/initialize: Generating new encryption key, since we did not find it on disk'
);
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key // https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
key = crypto.randomBytes(32).toString('hex'); key = crypto.randomBytes(32).toString('hex');
userConfig.set('key', key); userConfig.set('key', key);
} }
await sql.initialize({ configDir: userDataPath, key }); await sql.initialize({ configDir: userDataPath, key });
await sqlChannels.initialize({ userConfig }); await sqlChannels.initialize();
// const allAttachments = await attachments.getAllAttachments(userDataPath); async function cleanupOrphanedAttachments() {
// const orphanedAttachments = await sql.removeKnownAttachments(allAttachments); const allAttachments = await attachments.getAllAttachments(userDataPath);
// await attachments.deleteAll({ const orphanedAttachments = await sql.removeKnownAttachments(
// userDataPath, allAttachments
// attachments: orphanedAttachments, );
// }); await attachments.deleteAll({
userDataPath,
attachments: orphanedAttachments,
});
}
await attachmentChannel.initialize({
configDir: userDataPath,
cleanupOrphanedAttachments,
});
ready = true; ready = true;

@ -3,7 +3,7 @@
"productName": "Signal", "productName": "Signal",
"description": "Private messaging from your desktop", "description": "Private messaging from your desktop",
"repository": "https://github.com/signalapp/Signal-Desktop.git", "repository": "https://github.com/signalapp/Signal-Desktop.git",
"version": "1.15.5", "version": "1.16.0-beta.1",
"license": "GPL-3.0", "license": "GPL-3.0",
"author": { "author": {
"name": "Open Whisper Systems", "name": "Open Whisper Systems",
@ -40,6 +40,7 @@
"styleguide": "styleguidist server" "styleguide": "styleguidist server"
}, },
"dependencies": { "dependencies": {
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#ed4f4d179ac010c6347b291cbd4c2ebe5c773741",
"@sindresorhus/is": "^0.8.0", "@sindresorhus/is": "^0.8.0",
"archiver": "^2.1.1", "archiver": "^2.1.1",
"backbone": "^1.3.3", "backbone": "^1.3.3",
@ -49,10 +50,8 @@
"bunyan": "^1.8.12", "bunyan": "^1.8.12",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"config": "^1.28.1", "config": "^1.28.1",
"electron-config": "^1.0.0",
"electron-editor-context-menu": "^1.1.1", "electron-editor-context-menu": "^1.1.1",
"electron-is-dev": "^0.3.0", "electron-is-dev": "^0.3.0",
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#ed4f4d179ac010c6347b291cbd4c2ebe5c773741",
"electron-unhandled": "https://github.com/scottnonnenberg-signal/electron-unhandled.git#7496187472aa561d39fcd4c843a54ffbef0a388c", "electron-unhandled": "https://github.com/scottnonnenberg-signal/electron-unhandled.git#7496187472aa561d39fcd4c843a54ffbef0a388c",
"electron-updater": "^2.21.10", "electron-updater": "^2.21.10",
"emoji-datasource": "4.0.0", "emoji-datasource": "4.0.0",
@ -110,7 +109,7 @@
"asar": "^0.14.0", "asar": "^0.14.0",
"bower": "^1.8.2", "bower": "^1.8.2",
"chai": "^4.1.2", "chai": "^4.1.2",
"electron": "2.0.1", "electron": "2.0.8",
"electron-builder": "^20.13.5", "electron-builder": "^20.13.5",
"electron-icon-maker": "0.0.3", "electron-icon-maker": "0.0.3",
"eslint": "^4.14.0", "eslint": "^4.14.0",

@ -89,14 +89,14 @@
<hr> <hr>
<div class='spell-check-setting'> <div class='spell-check-setting'>
<h3>{{ spellCheckHeader }}</h3> <h3>{{ spellCheckHeader }}</h3>
<input type='checkbox' name='spell-check-setting' /> <input type='checkbox' name='spell-check-setting' id='spell-check-setting' />
<label for='spell-check-setting'>{{ spellCheckDescription }}</label> <label for='spell-check-setting'>{{ spellCheckDescription }}</label>
</div> </div>
<hr> <hr>
<div class='permissions-setting'> <div class='permissions-setting'>
<h3>{{ permissions }}</h3> <h3>{{ permissions }}</h3>
<div class='media-permissions'> <div class='media-permissions'>
<input type='checkbox' name='media-permissions' /> <input type='checkbox' name='media-permissions' id='media-permissions' />
<label for='media-permissions'>{{ mediaPermissionsDescription }}</label> <label for='media-permissions'>{{ mediaPermissionsDescription }}</label>
</div> </div>
</div> </div>

@ -125,6 +125,14 @@
color: $color-light-90; color: $color-light-90;
} }
.module-expire-timer {
background-color: $color-white-07;
}
.module-expire-timer--incoming {
background-color: $color-light-45;
}
.module-quote--incoming { .module-quote--incoming {
background-color: $color-signal-blue-025; background-color: $color-signal-blue-025;
border-left-color: $color-signal-blue; border-left-color: $color-signal-blue;
@ -286,6 +294,10 @@
color: $color-white; color: $color-white;
} }
.module-expire-timer--incoming {
background-color: $color-white-07;
}
.module-quote__primary__author { .module-quote__primary__author {
color: $color-light-90; color: $color-light-90;
} }

@ -1,13 +1,14 @@
### Countdown at different rates ### Countdown at different rates
```jsx ```jsx
<util.ConversationContext theme={util.theme}> <util.ConversationContext theme={util.theme} ios={util.ios}>
<li> <li>
<Message <Message
authorColor="cyan" authorColor="cyan"
direction="incoming" direction="incoming"
text="10 second timer" text="10 second timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 10 * 1000}
expirationLength={10 * 1000} expirationLength={10 * 1000}
expirationTimestamp={Date.now() + 10 * 1000} expirationTimestamp={Date.now() + 10 * 1000}
/> />
@ -18,6 +19,7 @@
authorColor="cyan" authorColor="cyan"
text="30 second timer" text="30 second timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 30 * 1000}
expirationLength={30 * 1000} expirationLength={30 * 1000}
expirationTimestamp={Date.now() + 30 * 1000} expirationTimestamp={Date.now() + 30 * 1000}
/> />
@ -28,6 +30,7 @@
direction="incoming" direction="incoming"
text="1 minute timer" text="1 minute timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 55 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 55 * 1000} expirationTimestamp={Date.now() + 55 * 1000}
/> />
@ -38,6 +41,7 @@
direction="incoming" direction="incoming"
text="5 minute timer" text="5 minute timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 5 * 60 * 1000}
expirationLength={5 * 60 * 1000} expirationLength={5 * 60 * 1000}
expirationTimestamp={Date.now() + 5 * 60 * 1000} expirationTimestamp={Date.now() + 5 * 60 * 1000}
/> />
@ -48,13 +52,14 @@
### Timer calculations ### Timer calculations
```jsx ```jsx
<util.ConversationContext theme={util.theme}> <util.ConversationContext theme={util.theme} ios={util.ios}>
<li> <li>
<Message <Message
authorColor="cyan" authorColor="cyan"
direction="incoming" direction="incoming"
text="Full timer" text="Full timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 60 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 60 * 1000} expirationTimestamp={Date.now() + 60 * 1000}
/> />
@ -65,6 +70,7 @@
status="delivered" status="delivered"
text="Full timer" text="Full timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 60 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 60 * 1000} expirationTimestamp={Date.now() + 60 * 1000}
/> />
@ -75,6 +81,7 @@
direction="incoming" direction="incoming"
text="55 timer" text="55 timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 55 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 55 * 1000} expirationTimestamp={Date.now() + 55 * 1000}
/> />
@ -85,6 +92,7 @@
status="delivered" status="delivered"
text="55 timer" text="55 timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 55 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 55 * 1000} expirationTimestamp={Date.now() + 55 * 1000}
/> />
@ -95,6 +103,7 @@
direction="incoming" direction="incoming"
text="30 timer" text="30 timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 30 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 30 * 1000} expirationTimestamp={Date.now() + 30 * 1000}
/> />
@ -105,6 +114,7 @@
status="delivered" status="delivered"
text="30 timer" text="30 timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 30 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 30 * 1000} expirationTimestamp={Date.now() + 30 * 1000}
/> />
@ -115,6 +125,7 @@
direction="incoming" direction="incoming"
text="5 timer" text="5 timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 5 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 5 * 1000} expirationTimestamp={Date.now() + 5 * 1000}
/> />
@ -125,6 +136,7 @@
status="delivered" status="delivered"
text="5 timer" text="5 timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 5 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 5 * 1000} expirationTimestamp={Date.now() + 5 * 1000}
/> />
@ -135,6 +147,7 @@
direction="incoming" direction="incoming"
text="Expired timer" text="Expired timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now()}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now()} expirationTimestamp={Date.now()}
/> />
@ -145,6 +158,7 @@
status="delivered" status="delivered"
text="Expired timer" text="Expired timer"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now()}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now()} expirationTimestamp={Date.now()}
/> />
@ -155,6 +169,7 @@
direction="incoming" direction="incoming"
text="Expiration is too far away" text="Expiration is too far away"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 120 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 120 * 1000} expirationTimestamp={Date.now() + 120 * 1000}
/> />
@ -165,6 +180,7 @@
status="delivered" status="delivered"
text="Expiration is too far away" text="Expiration is too far away"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() + 120 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 120 * 1000} expirationTimestamp={Date.now() + 120 * 1000}
/> />
@ -175,6 +191,7 @@
direction="incoming" direction="incoming"
text="Already expired" text="Already expired"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() - 20 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() - 20 * 1000} expirationTimestamp={Date.now() - 20 * 1000}
/> />
@ -185,6 +202,7 @@
status="delivered" status="delivered"
text="Already expired" text="Already expired"
i18n={util.i18n} i18n={util.i18n}
timestamp={Date.now() - 20 * 1000}
expirationLength={60 * 1000} expirationLength={60 * 1000}
expirationTimestamp={Date.now() - 20 * 1000} expirationTimestamp={Date.now() - 20 * 1000}
/> />

@ -81,6 +81,7 @@ export interface Props {
referencedMessageNotFound: boolean; referencedMessageNotFound: boolean;
}; };
authorAvatarPath?: string; authorAvatarPath?: string;
isExpired: boolean;
expirationLength?: number; expirationLength?: number;
expirationTimestamp?: number; expirationTimestamp?: number;
onClickAttachment?: () => void; onClickAttachment?: () => void;
@ -211,15 +212,22 @@ export class Message extends React.Component<Props, State> {
} }
} }
public componentDidUpdate() {
this.checkExpired();
}
public checkExpired() { public checkExpired() {
const now = Date.now(); const now = Date.now();
const { expirationTimestamp, expirationLength } = this.props; const { isExpired, expirationTimestamp, expirationLength } = this.props;
if (!expirationTimestamp || !expirationLength) { if (!expirationTimestamp || !expirationLength) {
return; return;
} }
if (this.expiredTimeout) {
return;
}
if (now >= expirationTimestamp) { if (isExpired || now >= expirationTimestamp) {
this.setState({ this.setState({
expiring: true, expiring: true,
}); });

@ -1647,15 +1647,6 @@ concat-stream@^1.5.0:
readable-stream "^2.2.2" readable-stream "^2.2.2"
typedarray "^0.0.6" typedarray "^0.0.6"
conf@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/conf/-/conf-1.1.1.tgz#238d0a3090ac4916ed2d40c7e81d7a11667bc7ba"
dependencies:
dot-prop "^4.1.0"
env-paths "^1.0.0"
make-dir "^1.0.0"
pkg-up "^2.0.0"
config@^1.28.1: config@^1.28.1:
version "1.28.1" version "1.28.1"
resolved "https://registry.yarnpkg.com/config/-/config-1.28.1.tgz#7625d2a1e4c90f131d8a73347982d93c3873282d" resolved "https://registry.yarnpkg.com/config/-/config-1.28.1.tgz#7625d2a1e4c90f131d8a73347982d93c3873282d"
@ -2423,12 +2414,6 @@ electron-chromedriver@~1.8.0:
electron-download "^4.1.0" electron-download "^4.1.0"
extract-zip "^1.6.5" extract-zip "^1.6.5"
electron-config@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/electron-config/-/electron-config-1.0.0.tgz#069d044cc794f04784ae72f12916725d3c8c39af"
dependencies:
conf "^1.0.0"
electron-download-tf@4.3.4: electron-download-tf@4.3.4:
version "4.3.4" version "4.3.4"
resolved "https://registry.yarnpkg.com/electron-download-tf/-/electron-download-tf-4.3.4.tgz#b03740b2885aa2ad3f8784fae74df427f66d5165" resolved "https://registry.yarnpkg.com/electron-download-tf/-/electron-download-tf-4.3.4.tgz#b03740b2885aa2ad3f8784fae74df427f66d5165"
@ -2538,9 +2523,9 @@ electron-updater@^2.21.10:
semver "^5.5.0" semver "^5.5.0"
source-map-support "^0.5.5" source-map-support "^0.5.5"
electron@2.0.1: electron@2.0.8:
version "2.0.1" version "2.0.8"
resolved "https://registry.yarnpkg.com/electron/-/electron-2.0.1.tgz#d9defcc187862143b9027378be78490eddbfabf4" resolved "https://registry.yarnpkg.com/electron/-/electron-2.0.8.tgz#6ec7113b356e09cc9899797e0d41ebff8163e962"
dependencies: dependencies:
"@types/node" "^8.0.24" "@types/node" "^8.0.24"
electron-download "^3.0.1" electron-download "^3.0.1"
@ -6428,12 +6413,6 @@ pkg-dir@^2.0.0:
dependencies: dependencies:
find-up "^2.1.0" find-up "^2.1.0"
pkg-up@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
dependencies:
find-up "^2.1.0"
pkginfo@0.4.0: pkginfo@0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.0.tgz#349dbb7ffd38081fcadc0853df687f0c7744cd65" resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.0.tgz#349dbb7ffd38081fcadc0853df687f0c7744cd65"

Loading…
Cancel
Save